1

I have a Prism based Xamarin Forms app that contains an edit page that is wrapped in a Navigation page so there is a back button at top left on both Android and iOS. To avoid the user accidentally losing an edit in progress by accidentally clicking the back button (in particular on Android) we want to prompt them to confirm that they definitely want to cancel.

Thing is, this seems like something that is not baked in to Xamarin forms. You can override OnBackButtonPressed in a navigation page, but that only gets called for the hardware/software back button on Android. There are articles detailing techniques to intercept the actual arrow button at the top left on Android (involving overriding OnOptionsItemSelected in the Android MainActivity), but on iOS I'm not sure it is even possible.

So I can't help but wonder if I am going about this the wrong way? Should I not be intercepting the top left / hardware / software back button in this way? Seems like a pretty common thing to do (e.g. press back when editing a new contact in the android built in Contacts app and you get a prompt) but it really feels like I am fighting the system here somehow.

There are previous questions around this, most relevant appears to be How to intercept Navigation Bar Back Button Clicked in Xamarin Forms? - but I am looking for some broad brush suggestions for an approach here. My objective is to show the user a page with the <- arrow at top left for Android, "Cancel" for iOS but I would like to get some views about the best way to go about it that does not involve me fighting against prism / navigation pages / xamarin forms and (where possible) not breaking the various "best practices" on Android and iOS.

fubaar
  • 2,489
  • 1
  • 21
  • 21

3 Answers3

1

After going down the same path as you and being told not to prevent users from going back, I decided on showing an alert after they tap the back button (within ContentPage.OnDisappearing()) that says something like Would you like to save your work?.

If you go with this approach, be sure to use Application.MainPage.DisplayAlert() instead of just this.DisplayAlert() since your ContentPage might not be visible at that point.

Here is how I currently handle saving work when they click the back button (I consolidated a good bit of code and changed some things):

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

    // At this point the page is gone or is disappearing, but all properties are still available

    #region Auto-save Check and Execution

    /*
     * Checks to see if any edits have been made and if a save is not in progress, if both are true, it asks if they want to save, if yes, it checks for validation errors.
     *  If it finds them, it marks it as such in the model before saving the model to the DB and showing an alert stating what was done
     */
    if(!_viewModel.WorkIsEdited || _viewModel.SaveInProgress) { //WorkIsEdited changes if they enter/change data or focus on certain elements such as a Picker
        return;
    }

    if(!await Application.Current.MainPage.DisplayAlert("ALERT", "You have unsaved work! Would you like to save now?", "Yes", "No")) {
        return;
    }

    if(await _viewModel.SaveClaimErrorsOrNotAsync()) { //The return value is whether validation succeeds or not, but it gets saved either way
        App.SuccessToastConfig.Message = "Work saved successfully. Try saving it yourself next time!";
        UserDialogs.Instance.Toast(App.SuccessToastConfig);
    } else if(await Application.Current.MainPage.DisplayAlert("ERROR", "Work saved successfully but errors were detected. Tap the button to go back to your work.", "To Work Entry", "OK")) {
        await Task.Delay(200);  //BUG: On Android, the alert above could still be displayed when the page below is pushed, which prevents the page from displaying //BUG: On iOS 10+ currently the alerts are not fully removed from the view hierarchy when execution returns (a fix is in the works)

        await Application.Current.MainPage.Navigation.PushAsync(new WorkPage(_viewModel.SavedWork));
    }

    #endregion
}
hvaughan3
  • 10,955
  • 5
  • 56
  • 76
  • Thanks for sharing your experience with this. Hasn't the page already disappeared at that point (i.e. the page visible behind is no longer the one they were editing)? – fubaar May 28 '17 at 10:24
  • @fubaar Technically yes, but the code in `OnDisappearing` is still able to be run. That is the point of that event so everything is still accessible, you just do not want to attempt to display an alert from that `ContentPage` because it will no longer be in the view hierarchy, but that does not mean that it is immediately destroyed or anything. So you can just use `Application.MainPage.DisplayAlert()` for displaying the alert. – hvaughan3 May 28 '17 at 16:44
  • Thanks for the additional info. So visually, the page is still visible behind the alert? How do you cancel the navigation in that scenario - would you have to navigate back to the page? – fubaar May 28 '17 at 21:31
  • @fubaar Once `OnDisappearing()` executes, the page is no longer visible so the alert appears on the page beneath it. In my scenario we are not canceling navigation. In our app, they hit back & they go to previous page, then the alert shows & we give the user the option to save their work or not. If they save it, we also validate the data they entered. If it fails validation, we still save the work but we then give them the option to go back to the work to fix it. If they choose to go back, we pull the previously saved work & pass it into the constructor of a new'ed up page. My edit might help. – hvaughan3 May 29 '17 at 00:32
  • @fubaar Another option is to hide your navigation bar and create your own, then you can have what ever button you want and have it do what ever you want but then you also need to worry about sizing and a few other things but it is something I have done in another app, though not because I wanted to prevent navigation. – hvaughan3 May 29 '17 at 01:13
0

What you ask for is not possible. The back button tap cannot be canceled on iOS even in native apps. You can do some other tricks like having a custom 'back' button, but in general you shouldn't do that - you should instead have a modal dialog with the Done and Cancel buttons (or something similar).

Ivan Ičin
  • 9,672
  • 5
  • 36
  • 57
  • That appears to be the consensus here for iOS - have a "Cancel" button on the left where the " – fubaar May 28 '17 at 10:27
  • @fubaar, as said in my answer you can create the button titled "<" and making it go back in the code after the prompt. That should work for Xamarin Forms, too. But you can't use the real "<" for that. – Ivan Ičin May 28 '17 at 13:15
  • Also you should be able to use the modal page: https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/navigation/modal/ – Ivan Ičin May 28 '17 at 14:11
  • My point is that you cannot place a button (e.g. one that says "Cancel") in the desired location (top left) in Xamarin Forms - without writing custom renderers. That goes for navigation or modal pages. i.e. there is no built in support for this scenario. – fubaar May 28 '17 at 21:32
0

If you use xamarin forms that code it is work.

CrossPlatform source

public class CoolContentPage : ContentPage
    {
        public Action CustomBackButtonAction { get; set; }

        public static readonly BindableProperty EnableBackButtonOverrideProperty =
               BindableProperty.Create(nameof(EnableBackButtonOverride), typeof(bool), typeof(CoolContentPage), false);            

        public bool EnableBackButtonOverride{
            get { return (bool)GetValue(EnableBackButtonOverrideProperty); }
            set { SetValue(EnableBackButtonOverrideProperty, value); }
        }
    }
}

Android source

public override bool OnOptionsItemSelected(IMenuItem item)
{

    if (item.ItemId == 16908332)
    {

       var currentpage = (CoolContentPage)
       Xamarin.Forms.Application.
       Current.MainPage.Navigation.
       NavigationStack.LastOrDefault();

       if (currentpage?.CustomBackButtonAction != null)
       {
         currentpage?.CustomBackButtonAction.Invoke();
         return false;
       }
       return base.OnOptionsItemSelected(item);
    }
    else
    {
        return base.OnOptionsItemSelected(item);
    }
}

public override void OnBackPressed()
{
    var currentpage = (CoolContentPage)
    Xamarin.Forms.Application.
    Current.MainPage.Navigation.
    NavigationStack.LastOrDefault();

    if (currentpage?.CustomBackButtonAction != null)
    {
        currentpage?.CustomBackButtonAction.Invoke();
    }
    else
    {
        base.OnBackPressed();
    }
}

iOS source

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

     if (((CoolContentPage)Element).EnableBackButtonOverride)
     {
          SetCustomBackButton();
     }
}

private void SetCustomBackButton()
{
     var backBtnImage = UIImage.FromBundle("iosbackarrow.png");

     backBtnImage = backBtnImage.ImageWithRenderingMode
     (UIImageRenderingMode.AlwaysTemplate);

     var backBtn = new UIButton(UIButtonType.Custom)
     {
          HorizontalAlignment =   
          UIControlContentHorizontalAlignment.Left,
          TitleEdgeInsets = 
          new UIEdgeInsets(11.5f, 15f, 10f, 0f),
          ImageEdgeInsets = 
          new UIEdgeInsets(1f, 8f, 0f, 0f)
     };

     backBtn.SetTitle("Back", UIControlState.Normal);
     backBtn.SetTitleColor(UIColor.White, UIControlState.Normal); 
     backBtn.SetTitleColor(UIColor.LightGray, UIControlState.Highlighted);
     backBtn.Font = UIFont.FromName("HelveticaNeue", (nfloat)17);
     backBtn.SetImage(backBtnImage, UIControlState.Normal);
     backBtn.SizeToFit();
     backBtn.TouchDown += (sender, e) =>
     {
          // Whatever your custom back button click handling
          if(((CoolContentPage)Element)?.
          CustomBackButtonAction != null)
          {    
            ((CoolContentPage)Element)?.
               CustomBackButtonAction.Invoke();
          }
     };
     backBtn.Frame = new CGRect(
          0,
          0,
          UIScreen.MainScreen.Bounds.Width / 4,
          NavigationController.NavigationBar.Frame.Height);
     var btnContainer = new UIView(
     new CGRect(0, 0, 
     backBtn.Frame.Width, backBtn.Frame.Height));
     btnContainer.AddSubview(backBtn);
     var fixedSpace = 
     new UIBarButtonItem(UIBarButtonSystemItem.FixedSpace)
     {
          Width = -16f
     };
     var backButtonItem = new UIBarButtonItem("",
     UIBarButtonItemStyle.Plain, null)
     {
          CustomView = backBtn
     };
     NavigationController.TopViewController.NavigationItem.LeftBarButtonItems  = new[] { fixedSpace, backButtonItem };
}

using in xamarin forms

 public Page2()
        {
            InitializeComponent();

            if (EnableBackButtonOverride)
            {
                this.CustomBackButtonAction = async () =>
                {
                    var result = await this.DisplayAlert(null, "Go back?" Yes go back", "Nope");

                    if (result)
                    {
                        await Navigation.PopAsync(true);
                    }
                };
            }
        }
Artem Tishchenko
  • 330
  • 1
  • 13