balintn
balintn

Reputation: 1423

Dotnet Maui: How to prevent navigating away from a page with unsaved data?

Implementing a Maui app with Shell navigation, I have a page with possibly unsaved data. When the user navigates away from the page, I want to ask them what they want to do (navigate away and discard data or stay on the page - or some other, similar choices).

Environment is VS 2022, Maui v7.0.

I've seen articles handling the back button, but there are other ways of leaving the page, like controls on the page itself or using the flyout menu.

I'd like the solution to work in any case, regardless of how the navigation is initiated. How can I do this?

Upvotes: 2

Views: 3124

Answers (1)

balintn
balintn

Reputation: 1423

I ended up using the Page.OnNavigating() event, as it is fired every time, navigation happens from my page.

There are some caveats though: the event handler needs to be registered and unregistered in a tricky way to prevent multiple registrations.

Within the event handler, I display a user dialog. For the duration of the user interaction, the navigation has to be paused, and either carried out or cancelled based on the user's decision. There is a (as of this writing) undocumented method on the event arguments called GetDeferral(), I needed to use that.

Here's how I implemented it.

Event handler registration

I tried registering in the constructor, unregistering in the destructor, but it didn't work. I use dependency injection, page is registered as transient. When navigating away from the page, the destructor is not called yet, a second viewing of the page registers the event handler for a second time. Changing page registration to a singleton caused issues in page control lifetimes. Ended up registering/unregistering in the OnAppearing/OnDisappearing methods:

In MyPage.xaml.cs:

public partial class TablePartyPage : ContentPage, IDisposable
{
    private MyViewModel _vm;
    [...]
    
    protected override void OnAppearing()
    {
        base.OnAppearing();
        Shell.Current.Navigating += OnNavigating;
    }

    protected override void OnDisappearing()
    {
        Shell.Current.Navigating -= OnNavigating;
        base.OnDisappearing();
    }
    
    private async void OnNavigating(object sender, ShellNavigatingEventArgs args)
    {
        // I define constants for page routes and register them in AppShell,
        // here I use those routes to allow certain navigation targets
        if (args.Target.Location.OriginalString.EndsWith(AppShell.MyPageRoute)
            || args.Target.Location.OriginalString.EndsWith(AppShell.MyChildPageRoute))
        {   // Navigating to this page or one of its children - allow this navigation
            return;
        }
        else
        {   // Navigating away from this page
            if (!_vm.HasUnsavedChanges)
                return; // No unsaved changes - allow navigation

            // Pause navigation till the user makes a decision
            var deferral = args.GetDeferral();

            const string STAY = "Stay on this page";
            const string DISCARD = "Discard changes";
            switch (await DisplayActionSheet(
                "There are unsaved changes on this page", null, null, STAY, DISCARD))
            {
                case STAY:
                default:
                    // Do not leave this page, cancel navigation
                    args.Cancel();
                    break;

                case DISCARD:
                    _vm.DiscardChanges();
                    break;
            }
            
            // Un-pause navigation
            deferral.Complete();
        }
    }
}

Upvotes: 3

Related Questions