Forum Xamarin.Forms

Lifecycle events with modal popups

TommigunTommigun Member ✭✭

Hi.

What is the recommended pattern for setting things up in a page in an asynchronous manner, that does not get retriggered when a modal dialog has been dismissed on top of the page?

I currently do:
public partial class MyPage : ContentPage
{
...
private bool hasAppeared;
...

    protected override async void OnAppearing()
    {
        base.OnAppearing();

        if (this.hasAppeared) // for modal popups
            return;
        this.hasAppeared = true;

      // my code here...
    }
}

but surely there's a better way? I was looking for overloads of OnAppearing with some state information being sent in there but none seem to exist.

Thanks!

Best Answer

  • TommigunTommigun Member ✭✭
    edited March 2020 Accepted Answer

    According to the Github discussion this is not a bug, so to get around this I wrote the following helper class. Feel free to use if you run into the same problem:

    using Xamarin.Forms;
    
    namespace MyNamespace
    {
        public abstract class ContentPageExtendedLifeCycle : ContentPage
        {
            private int modalStackPushedCount;
    
            protected override void OnAppearing()
            {
                base.OnAppearing();
    
                Application.Current.ModalPushing -= Current_ModalPushing;
                Application.Current.ModalPushing += Current_ModalPushing;
                Application.Current.ModalPopped -= Current_ModalPopped;
                Application.Current.ModalPopped += Current_ModalPopped;
    
                this.OnAppearing(this.modalStackPushedCount > 0);
            }
    
            protected override void OnDisappearing()
            {
                base.OnDisappearing();
    
                Application.Current.ModalPushing -= Current_ModalPushing;
                Application.Current.ModalPopped -= Current_ModalPopped;
    
                this.OnDisappearing(this.modalStackPushedCount > 0);
            }
    
            private void Current_ModalPushing(object sender, ModalPushingEventArgs e)
            {
                this.modalStackPushedCount++;
            }
            private void Current_ModalPopped(object sender, ModalPoppedEventArgs e)
            {
                System.Diagnostics.Debug.Assert(this.modalStackPushedCount > 0);
                this.modalStackPushedCount--;
            }
    
            protected virtual void OnAppearing(bool causedByModalPop) { }
            protected virtual void OnDisappearing(bool causedByModalPush) { }
        }
    }
    

    Usage: Simply derive your pages from ContentPageExtendedLifeCycle instead of a ContentPage, and use as such:

    protected override void OnAppearing(bool causedByModalPop)
    {
        if (!causedByModalPop)
            // your init code goes here
    }
    
    protected override void OnDisappearing(bool causedByModalPush)
    {
        if (!causedByModalPush)
            // your cleanup code goes here
    }
    

Answers

  • YelinzhYelinzh Member, Xamarin Team Xamurai
    edited March 2020

    that does not get retriggered when a modal dialog has been dismissed on top of the page

    The OnAppearing method will be triggerred as the Page disappears. You can add a breakpoint to vertify that.

    Please check if the value of hasAppeared is correct. You could set the property to be static and public. Then change the value in the popup page's OnDisappearing method or OnBackButtonPressed method.

    public partial class Popup_Page : ContentPage
    {
        public Popup_Page()
        {
            InitializeComponent();
        }
    
        protected override void OnDisappearing()
        {
            base.OnDisappearing();
            MainPage.hasAppeared = true;
        }
    }
    
  • TommigunTommigun Member ✭✭
    edited March 2020

    Hi and thanks for the reply, and also sorry for being so unclear.

    What I meant was that OnAppearing and OnDisappearing being triggered on pages when a modal dialog is pushed on top of them is inconvenient. I'd rather not play around with a lot of state, so I was hoping an overload would exist that sends in what caused the even to be triggered, something like:
    void OnAppearing(bool navigationStackChanged)
    , in which case I could just ignore the event if it was invoked by the modal stack changing.

    My app uses a 'main page', where its OnAppearing is an asynchronous setup method that sets up view models and the state of the app. OnDisappearing cleans things up. As my app has the concept of sessions, the main page gets destroyed if the user logs out or a session gets invalidated.

    Now, because OnAppearing/OnDisappearing are called on the main page when a modal dialog is pushed, it always does the cleanup and initialisation when I display any kind of popup. One could even make a case for the main page not actually disappearing when a modal dialog is pushed, especially if the form sheet modal style is used (you can clearly see the page under the modal dialog), so why is an unaffected page's OnAppearing/OnDisappearing even being called?

    What I want to accomplish is to detect, in OnAppearing and OnDisappearing, whether the page really is appearing or disappearing, or if they are just called because the modal stack changed or whatever other conditions may invoke them. I can use the flag hack as long as I don't have an OnDisappearing, but unfortunately this is not the case here.
    In other words, detect if OnAppearing and OnDisappearing were invoked because of the page was actually being pushed to or popped from the navigation stack, or if the modal stack change invoked them (which I still don't understand why it does that).

    There must be a good pattern for that which does not involve private flags and changing state, as it's such a common thing to do.
    I really don't want to use static flags and such things if they can at all be avoided. I don't think I can even make them robust - maybe I could count the number of appearing and disappearing, as actually closing the main page while a popup is on top of it is a valid case... But it seems like a smelly pattern that will absolutely break (especially if you need it for any more pages than one), so thanks for further insights!

  • NMackayNMackay GBInsider, University admin
    edited March 2020

    @Tommigun

    the modal lifecycle events are wrong and have been for years, use Prism if you you don't want to code your way around the limitation (they have).

    @Jarvan @ColeX You need to pressure the Forms internal team more because basics like this rarely work, try using a bindable picker in a ContentView in UWP, (ImageFromSteam critical memory leak in UWP) and 100s of other open issues since 2016!! Fed up of highlighting these shortcomings that rarely get fixed. When everyone moves to Flutter you'll be wondering why!

  • TommigunTommigun Member ✭✭
    edited March 2020
  • TommigunTommigun Member ✭✭
    edited March 2020 Accepted Answer

    According to the Github discussion this is not a bug, so to get around this I wrote the following helper class. Feel free to use if you run into the same problem:

    using Xamarin.Forms;
    
    namespace MyNamespace
    {
        public abstract class ContentPageExtendedLifeCycle : ContentPage
        {
            private int modalStackPushedCount;
    
            protected override void OnAppearing()
            {
                base.OnAppearing();
    
                Application.Current.ModalPushing -= Current_ModalPushing;
                Application.Current.ModalPushing += Current_ModalPushing;
                Application.Current.ModalPopped -= Current_ModalPopped;
                Application.Current.ModalPopped += Current_ModalPopped;
    
                this.OnAppearing(this.modalStackPushedCount > 0);
            }
    
            protected override void OnDisappearing()
            {
                base.OnDisappearing();
    
                Application.Current.ModalPushing -= Current_ModalPushing;
                Application.Current.ModalPopped -= Current_ModalPopped;
    
                this.OnDisappearing(this.modalStackPushedCount > 0);
            }
    
            private void Current_ModalPushing(object sender, ModalPushingEventArgs e)
            {
                this.modalStackPushedCount++;
            }
            private void Current_ModalPopped(object sender, ModalPoppedEventArgs e)
            {
                System.Diagnostics.Debug.Assert(this.modalStackPushedCount > 0);
                this.modalStackPushedCount--;
            }
    
            protected virtual void OnAppearing(bool causedByModalPop) { }
            protected virtual void OnDisappearing(bool causedByModalPush) { }
        }
    }
    

    Usage: Simply derive your pages from ContentPageExtendedLifeCycle instead of a ContentPage, and use as such:

    protected override void OnAppearing(bool causedByModalPop)
    {
        if (!causedByModalPop)
            // your init code goes here
    }
    
    protected override void OnDisappearing(bool causedByModalPush)
    {
        if (!causedByModalPush)
            // your cleanup code goes here
    }
    
  • NMackayNMackay GBInsider, University admin

    "According to the Github discussion this is not a bug, so to get around this"

    It's not a bug, it's a lack of lifecycle support in Forms, not surprising your suggestion was turned down, Shell/Vanilla Forms has no useful lifecycle hooks that devs needs, it's okay until you want to do proper mvvm apps and address memory management. Prism gives the hooks for that. Forms does give some hooks but you have to write your own wrapper that Shell doesn't provide.

Sign In or Register to comment.