Xamarin Forms Back Navigation Pitfalls

For those new to cross platform development like I once was, I am going to go through the pitfalls I made while setting up my back navigation. I am a Windows Phone user and because the development environment I always work in first is Windows Phone due to its ease of debugging, I built an app thinking all other platforms were mostly the same. Unfortunately I found out the hard way, they are some significant differences.

Back Button Defaults

  • iOS = Software Back Button Only
    • Bonus: Hidden swipe to navigate back, that also goes crazy when you have a MasterDetailPage.
  • Android = Software and Hardware Back Button
  • WinPhone = Hardware Back Button

You may also be familiar with the OnBackButtonPressed override on a ContentPage. However if you read the fine print, it only works with a hardware back button. This means it works for Windows Phone 100%, Android is a 50/50 depending on what the user presses and not at all for iOS. It is not well suited for cross platform compatibility.

Stop Back Navigation

If you are just wanting to stop people from going back you can do this to cover yourselves on all platforms, however I much prefer you let the user do what you want rather than overriding them.

  1. On Windows, override the OnBackButtonPressed and always return true to stop the back proceeding. This also accommodates for the Android hardware back button.
  2. Use NavigationPage.SetHasBackButton(page, false) to remove all software back buttons from iOS and Android.
  3. Remove the ability to back swipe in iOS.
[assembly: ExportRenderer(typeof(NavigationPage), typeof(CustomNavigationRenderer))]
namespace Mobile.iOS.CustomRenderer
{
    public class CustomNavigationRenderer : NavigationRenderer
    {
        public override void SetViewControllers(UIViewController[] controllers, bool animated)
        {
              base.SetViewControllers(controllers, animated);
              foreach (var controller in controllers)
              {
                 // Disable swipe back
                 ((UINavigationController)controller).InteractivePopGestureRecognizer.Enabled = false;
              }
         }
         public override void ViewDidLoad()
         {
              base.ViewDidLoad();
              if (InteractivePopGestureRecognizer != null)
              {
                  InteractivePopGestureRecognizer.Enabled = false;
              }
          }
    }
}

Back Navigation Design Considerations

In my scenario where I really screwed up, I used the OnBackButtonPressed override on the ContentPage, then put logic in there to determine if they were allowed to go back. To avoid this you should design your app with these considerations:

  • Switch Navigation Stacks if you want to stop people going back. Going modal.
  • Design pages where people must choose to save information or not, showing only a Save or Cancel button with no Back Button visible and a new navigation stack as mentioned above.
  • Make pages automatically save with each change. This is good for settings pages where when you move a toggle or enter a value it automatically saves the result, meaning no save or cancel options are needed.

Dirty Hacks

Now lets say you got way too far in your design to go back, how do you tap into the back button early enough to stop it from popping the page on iOS and Android? Please note this isn’t recommended but its a way out of a bad situation or really tight deadline.

Note: In the examples below I had a base ViewModel with an OnBackButtonPressed function that returned the result of if it was allowed to go back. You need to create this ViewModel yourself and place your own logic inside to determine if they should go back. In my OnBackButtonPressed I made it show a dialog that asks if the users wants to save or cancel changes.

Windows

You don’t need to do anything here, just use the OnBackButtonPressed override on the ContentPage.

Android

OnBackButtonPressed works for the Android hardware button but it doesn’t for the software back button. In your MainActivity.cs implement the below override.

public override bool OnOptionsItemSelected(IMenuItem item)
    {
        var app = Application.Current;
        if (item.ItemId == 16908332) // This makes me feel dirty.
        {
            var navPage = ((app.MainPage.Navigation.ModalStack[0] as MasterDetailPage).Detail as NavigationPage);

            if (app != null && navPage.Navigation.NavigationStack.Count > 0)
            {
                int index = navPage.Navigation.NavigationStack.Count - 1;

                var currentPage = navPage.Navigation.NavigationStack[index];

                var vm = currentPage.BindingContext as ViewModel;

                if (vm != null)
                {
                    var answer = vm.OnBackButtonPressed();
                    if (answer)
                        return true;
                }

            }
        }

        return base.OnOptionsItemSelected(item);
    }

iOS

You must create a new custom renderer for your pages and override the ViewWillAppear to override and place in your own back button. I made each page inherit from a CorePage and it has a custom property OverrideBackButton, so it would only override it if you set that value to true.

[assembly: ExportRenderer(typeof(Page), typeof(CustomPageRenderer))]
namespace Mobile.iOS.CustomRenderer
{
    public class CustomPageRenderer : PageRenderer
    {

        public override void ViewWillAppear(bool animated)
        {
            base.ViewWillAppear(animated);

            var page = Element as CorePage;

            if (page != null)
            {
                if ((page).OverrideBackButton)
                {
                    var root = this.NavigationController.TopViewController;
                    // NOTE: this doesn't look exactly right, you need to create an image to replicate the back arrow properly
                    root.NavigationItem.SetLeftBarButtonItem(new UIBarButtonItem("< Back", UIBarButtonItemStyle.Plain, (sender, args) =>
                    {
                        var navPage = page.Parent as NavigationPage;
                        var vm = page.BindingContext as ViewModel;

                        if (vm != null)
                        {
                            var answer = vm.OnBackButtonPressed();

                            if (!answer)
                                navPage.PopAsync();
                        }
                        else
                            navPage.PopAsync();
                    }), true);
                }
            }    
        }
    }
}
Microsoft MVP | Xamarin MVP | Xamarin Certified Developer |
Exrin MVVM Framework | Xamarin Forms Developer | Melbourne, Australia

Related Posts

19 Comments

  1. Rob

    If I was at Xamarin, I would had chosen a better name for the method, something like “OnDeviceBackButtonPressed” or “OnHardwareBackButtonPressed”, even if it’s longer, it clearly states what it’s meant for. “OnBackButton” is misleading. On the other hand, yes, always read the documentation 🙂

    By the way, your articles are great, keep them coming.

  2. Rob

    Additionally to your code(thanks a lot for sharing), here’s my trick for Android software button which works great:

    using Xamarin.Forms.Platform.Android.AppCompat;

    // In order to catch back navigation we set a listener on the toolbar instance set by NavigationPageRenderer
    // Looking at its source code, NavigationPageRenderer recreates the toolbar in OnLayout and when configuration changed
    public class NavigationPageRendererHack : NavigationPageRenderer, Android.Views.View.IOnClickListener
    {
    static readonly FieldInfo ToolbarFieldInfo;

    bool _disposed;
    AToolbar _toolbar;

    static NavigationPageRendererHack()
    {
    // get _toolbar private field info
    ToolbarFieldInfo = typeof(NavigationPageRenderer).GetField(“_toolbar”, BindingFlags.NonPublic | BindingFlags.Instance);
    }

    public void OnClick(Android.Views.View v)
    {
    // Call the NavigationPage which will trigger the default behavior
    // The default behavior is to navigate back if the Page derived classes return true from OnBackButtonPressed override
    Element.SendBackButtonPressed();
    }

    protected override void OnLayout(bool changed, int l, int t, int r, int b)
    {
    base.OnLayout(changed, l, t, r, b);

    UpdateToolbarInstance();
    }

    protected override void OnConfigurationChanged(Configuration newConfig)
    {
    base.OnConfigurationChanged(newConfig);

    UpdateToolbarInstance();
    }

    protected override void Dispose(bool disposing)
    {
    if (disposing && !_disposed)
    {
    _disposed = true;

    RemoveToolbarInstance();
    }

    base.Dispose(disposing);
    }

    void UpdateToolbarInstance()
    {
    RemoveToolbarInstance();
    GetToolbarInstance();
    }

    void GetToolbarInstance()
    {
    _toolbar = (AToolbar)ToolbarFieldInfo.GetValue(this);
    //var mi = t.GetMethod(“BarOnNavigationClick”, BindingFlags.NonPublic | BindingFlags.Instance);
    _toolbar.SetNavigationOnClickListener(this);
    }

    void RemoveToolbarInstance()
    {
    if (_toolbar != null)
    {
    _toolbar.SetNavigationOnClickListener(null);
    _toolbar = null;
    }
    }
    }

      1. Rob

        I’m not happy for having to use reflection. The main issue is that the implementation of NavigationPageRenderer doesn’t send Element.SendBackButtonPressed(); when toolbar button is clicked. It just pops the page.

  3. Habib Ali

    Hi Adam,
    Thanks for such useful info.
    I am facing an issue.
    The hack for windows
    “You don’t need to do anything here, just use the OnBackButtonPressed override on the ContentPage.”
    is not working
    Can anybody help ?

    Regards

    Habib Ali

    1. Adam Pedley

      If you have ContentPage inside a NavigationPage and you click the hardware back button, the OnBackButtonPressed event will be fired. What is your setup and how are you going back?

      1. Habib Ali

        Thank you so much for the fast reply. I am using windows surface pro not windows phone. I also tried running it on local machine. I have content page inside Navigation Page

        1. Habib Ali

          I am also using the software button instead of hardware button as there is no hardware button for back on windows surface pro tablet

          1. Adam Pedley

            It shouldn’t matter with UWP. I just tested it locally. If you press the back button in the navigation bar at the top left hand corner, the OnBackButton pressed even is called.

  4. Jérémy BRUN-PICARD

    Hi, thanks for your code.
    On Android side, maybe checking if (item.ItemId == Android.Resource.Id.Home) should do the trick a cleaner way than if (item.ItemId == 16908332)
    Thanks again!

  5. Dennis Welu

    Great summary Adam.

    I have a design with a model that is n-level hierarchical in nature and the user may or may not choose to edit a node as they drill down into it. So showing a potential n-level set of modal navigation stacks really isn’t great. Plus I found on UWP desktop that the modal presentation automatically changes the presentation style of the UI which isn’t cool either. (e.g. the vertical spacing of ListView items is tighter).

    In my case rather than replacing the software back button I just remove it (NavigationPage.SetHasBackButton(this, false);) and add save/cancel as right bar button items when the user starts editing. This forces them to make the same choice they otherwise would in a modal design, but they stay in the main navigation hierarchy. Since there is no software back button the Android workaround code is not needed. But the OnBackButtonPressed still needs to be handled, which at least can be done without platform specific code. The only platform specific part then is disabling the swipe gesture on iOS dynamically. At least until I find a problem with it… 🙂

  6. Jack

    Hi Adam,
    I am trying use the hack code snippet in the android code the line

    var navPage = ((app.MainPage.Navigation.ModalStack[0] as MasterDetailPage).Detail as NavigationPage);

    I am getting “app” as undefined variable and

    in iOS code

    var page = Element as CorePage;

    CorePage is not recognized. I thought its Page from Xamarin.Forms.Core assembly but it dont have a “OverrideBackButton” property.

    Please clarify the proper initialization of these variables.

    Or please share a full working project if any.

    1. Adam Pedley

      Sorry, if he posted a link it would most likely have been eaten up by the SPAM filter. I found that you can remove 99% of spam, by deleting any comment that has a url in it.

Leave A Comment?