MvvmCross migration causing a Xamarin Custom iOS View Presenter issue - xamarin

While creating a CustomIosViewPresenter (of type MvxIosViewPresenter), in MVVMCross 5.x, there was a Show override that I was able to use to get the IMvxIosView so as to update the UIViewController presentation style using the PresentationValues from the ViewModel.
I had this code and it worked:
// Worked before
public override void Show(IMvxIosView view, MvvmCross.ViewModels.MvxViewModelRequest request)
{
if (request.PresentationValues != null)
{
if (request.PresentationValues.ContainsKey("NavigationMode") &&
request.PresentationValues["NavigationMode"] == "WrapInModalWithNavController")
{
var vc = view as IModalPresentation;
vc.ModalPresentationAttribute = new MvxModalPresentationAttribute
{
WrapInNavigationController = true,
ModalPresentationStyle = UIModalPresentationStyle.OverFullScreen,
ModalTransitionStyle = UIModalTransitionStyle.CoverVertical
};
}
}
base.Show(view, request);
}
But after migrating to MvvmCross 7.1, the older override doesn't work anymore and I have to use this instead, but there is no view passed into the Show override, how do I get it?
I tried this code below, but view is null and it's not able to cast it this way var view = viewType as IMvxIosView;
// Doesn't work now
public override Task<bool> Show(MvxViewModelRequest request)
{
if (request.PresentationValues != null)
{
if (request.PresentationValues.ContainsKey("NavigationMode") &&
request.PresentationValues["NavigationMode"] == "WrapInModalWithNavController")
{
var viewsContainer = Mvx.IoCProvider.Resolve<IMvxViewsContainer>();
var viewType = viewsContainer.GetViewType(request.ViewModelType);
var view = viewType as IMvxIosView;
var vc = view as IModalPresentation;
vc.ModalPresentationAttribute = new MvxModalPresentationAttribute
{
WrapInNavigationController = true,
ModalPresentationStyle = UIModalPresentationStyle.OverFullScreen,
ModalTransitionStyle = UIModalTransitionStyle.CoverVertical
};
}
}
return base.Show(request);
}
The reason I need this is because without this function when I close the special flow of view controllers that need this, its not closing all the view controllers in that flow, it closes only one of them at a time.

What you would normally do with MvvmCross if you want to navigate within a Modal ViewController is firstly add a MvxModalPresentationAttribute to the modal that will host the rest of the navigation where you set WrapInNavigationController to true.
For the children, it would just be regular child navigation, no attributes needed.
If you then want to control how the modal is popping you would create your own MvxPresentationHint and register it in your presenter using AddPresentationHintHandler.
Then you would in your ViewModel where you want to change the presentation call NavigationService.ChangePresentation(your hint).
As for the Presentation Hint, it should probably just call CloseModalViewControllers and that would probably do what you want.

TLDR: Feel for the developers that will come after you and build stuff the right way
So I dug into the MvvmCross MvxIosViewPresenter source code and was able to use this new override CreateOverridePresentationAttributeViewInstance()
I needed the request object to see the presentation values so I updated the Show function that gets called before the other override as follows:
MvxViewModelRequest _request;
public override Task<bool> Show(MvxViewModelRequest request)
{
_request = request;
return base.Show(request);
}
And I was able to get the ViewController this way, in order to selectively present it as a modal:
{
var view = base.CreateOverridePresentationAttributeViewInstance(viewType);
if (_request.PresentationValues.ContainsKey("NavigationMode") &&
_request.PresentationValues["NavigationMode"] == "WrapInModalWithNavController")
{
var vc = view as IModalPresentation;
vc.ModalPresentationAttribute = new MvxModalPresentationAttribute
{
WrapInNavigationController = true,
ModalPresentationStyle = UIModalPresentationStyle.OverFullScreen,
ModalTransitionStyle = UIModalTransitionStyle.CoverVertical
};
return vc;
}
return view;
}
And then the closing of the modal was another challenge, that I was able to figure out using the TryCloseViewControllerInsideStack and ChangePresentation overrides

Related

What is the difference between WKNavigationDelegate and WKUIDelegate

Can I use both of them in a project?
I need to override WKUIDelegate's CreateWebView method in order to open target=_blank links:
public override WKWebView CreateWebView(WKWebView webView, WKWebViewConfiguration configuration, WKNavigationAction navigationAction, WKWindowFeatures windowFeatures)
{
var url = navigationAction.Request.Url;
if (navigationAction.TargetFrame == null)
{
webView.LoadRequest(navigationAction.Request);
}
return null;
}
When I use WKUIDelegate in a demo it works (opens target _blank). But in real project they used WKNavigationDelegate too. And applying WKUIDelegate CreateWebView doesn't work.
OnElementChange in the renderer is like this:
var config = new WKWebViewConfiguration { };
webView = new WKWebView(Frame, config);
// Set the delegate here
webView = new WKWebView(this.Frame, new WKWebViewConfiguration());
webView.ScrollView.ScrollEnabled = true;
webView.ScrollView.Bounces = true;
webView.NavigationDelegate = new DisplayLinkWebViewDelegate();
webView.UIDelegate = MyWkWebViewDelegate();
SetNativeControl(webView);
WKNavigationDelegate : It helps you implement custom behaviors that are triggered during a web view's process of accepting, loading, and completing a navigation request.
And the WKUIDelegate class provides methods for presenting native user interface elements on behalf of a webpage.
The webpage here is not the webview ,but the html which been loaded on webview.
As we can see in following image
The method in WKUIDelegate are all associated with JS.
For more details about the two protocols you can check https://developer.apple.com/documentation/webkit/wkuidelegate?language=objc
and
https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc
if you want to do something when the webview finished loading, you can implement the method DidFinishNavigation in WKNavigationDelegate .
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
if(!webView.IsLoading)
{
// do some thing you want
}
}

Create ContentPage template with fixed view on top

Our Xamarin.Forms app works online and offline by downloading an original database to the cell phone and then syncing the SQLite database with the online database.
Our users need a way to see if they are online and if the changes they made got uploaded to the online database. What I try to achieve is to show the sync status at the top of every ContentPage, so the users can see this information all the time while working with the app.
What I tried is this: create a class "SyncInfoContentPage" that inherits from ContentPage. All ContentPages I already wrote will now not inherit from ContentPage anymore but from SyncInfoContentPage.
The SyncInfoContentPage automatically takes its Content and replaces it with a new Stacklayout that includes the SyncInfo and the original content. By doing this I don't have to rewrite the 77 ContentPages we already have.
This code works fine on Android, but on iOS the SyncInfo is not visible and (even worse) my ContentPages that inherit from SyncInfoContentPage do not react to anything anymore.
Here is my code:
public class SyncInfoContentPage : ContentPage
{
private readonly Frame SyncInfo;
public SyncInfoContentPage()
{
SyncInfo = BuildSyncInfo(); //Creates the frame with the sync Information
PropertyChanged += SyncInfoContentPage_PropertyChanged;
}
private void SyncInfoContentPage_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// Add the SyncInfo Frame on top of the Content when the Content gets changed
if (e.PropertyName.Equals("Content"))
{
bool change = false;
// If the content already is a StackLayout, check if the SyncInfo already got added, so that theres no infinite loop.
if (Content is StackLayout)
{
var check = Content as StackLayout;
if (!check.Children.Contains(SyncInfo))
{
change = true;
}
}
else // if the Content is no StackLayout, the SyncInfo Frame can't be inside the Content yet
{
change = true;
}
if (change)
{
var layout = Content; // This is a reference, probably the error?
Device.BeginInvokeOnMainThread(() =>
{
Content = new StackLayout
{
Children = { SyncInfo, layout }
};
});
}
}
}
}
The problem is probably that iOS doesn't like this part:
var layout = Content;
Content = new StackLayout { Children = { SyncInfo, layout } };
Thanks in advance for your help and any suggestions :-)
I got it to work. The solution is simple but strange. You have to add the original Content after you added the SyncInfo status bar.
var layoutOld = Content;
var layoutNew = new StackLayout
{
Children = { SyncInfo }
};
Content = layoutNew;
layoutNeu.Children.Add(layoutOld);

How Clear Back Stack on Xamarin IOS?

When a user authenticates correctly, it will be directed to the HomeViewModel. I want to remove the possibility that it can return to the login screen so I have created a Custom Presenter to remove all the screens that are below the new screen.
The implementation is as follows:
public class CustomPresenter: MvxFormsIosPagePresenter
{
public CustomPresenter(UIWindow window, MvxFormsApplication mvxFormsApp)
: base(window, mvxFormsApp)
{
}
public override void Show(MvxViewModelRequest request)
{
if (request.PresentationValues?["NavigationCommand"] == "StackClear")
{
var navigation = FormsApplication.MainPage.Navigation;
Debug.WriteLine("Navigation Back Stack Count -> " + navigation.NavigationStack.Count());
navigation.PopToRootAsync();
Debug.WriteLine("Navigation Back Stack Count After PopToRootAsync -> " + navigation.NavigationStack.Count());
return;
}
base.Show(request);
}
}
When the authentication process finishes correctly, I navigate to the home screen by passing a bundle with this special command:
LoginWithFacebookCommand.Subscribe(token => {
Debug.WriteLine("JWT Token -> " + token);
_userDialogs.ShowSuccess(AppResources.Login_Success);
var mvxBundle = new MvxBundle(new Dictionary<string, string> { { "NavigationCommand", "StackClear" } });
ShowViewModel<HomeViewModel>(presentationBundle: mvxBundle);
});
The problem is that it does not change the screen, it stays in the current one. What would be the way to do it correctly ?.
I am using MvvmCross 5.1.1 and MvvmCross.Forms 5.1.1
Thank you very much in advance.
As I understand it, PopToRootAsync() pops everything off the stack to the root. Which means you should then push your view that you wish to navigate to, onto your stack after that method is called i.e. use PushViewController(yourViewController) afterwards. Also, you should be using the new IMvxNavigationService by MvvmCross. You can give this a try:
var navigationService = Mvx.Resolve<IMvxNavigationService>();
LoginWithFacebookCommand.Subscribe(async (token) => {
Debug.WriteLine("JWT Token -> " + token);
_userDialogs.ShowSuccess(AppResources.Login_Success);
await navigationService.Navigate<HomeViewModel>();
});
To clear the backstack you basically need to override the Show method in the presenter and check whether your viewmodel is being called. If it is then set a new array of viewControllers. (Credit to #pnavk!!)
public class CustomPresenter : MvxIosViewPresenter
{
public override void Show(IMvxIosView view, MvxViewModelRequest request)
{
if (MasterNavigationController != null && view.ViewModel.GetType() == typeof(HomeViewModel))
{
var viewController = view as UIViewController;
MasterNavigationController.SetViewControllers(new UIViewController[] { viewController }, true);
}
else
base.Show(view, request);
}
}
Try this:
navigation.SetViewControllers(new UIViewController[] { vc }, true);
vc is the ViewController you want to set as the root of the navigation stack. You will need to get a reference to it which you can using the ViewControllers property on the NavigationController.
true - means you want to animate.

Should I be thinking differently about cancelling the back button in Xamarin Forms

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.
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
}
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).
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);
}
};
}
}

How to handle/cancel back navigation in Xamarin Forms

I tried to use the back navigation by overriding OnBackButtonPressed, but somehow it wasn't get called at all. I am using the ContentPage and the latest 1.4.2 release.
Alright, after many hours I figured this one out. There are three parts to it.
#1 Handling the hardware back button on android. This one is easy, override OnBackButtonPressed. Remember, this is for a hardware back button and android only. It will not handle the navigation bar back button. As you can see, I was trying to back through a browser before backing out of the page, but you can put whatever logic you need in.
protected override bool OnBackButtonPressed()
{
if (_browser.CanGoBack)
{
_browser.GoBack();
return true;
}
else
{
//await Navigation.PopAsync(true);
base.OnBackButtonPressed();
return true;
}
}
#2 iOS navigation back button. This one was really tricky, if you look around the web you'll find a couple examples of replacing the back button with a new custom button, but it's almost impossible to get it to look like your other pages. In this case I made a transparent button that sits on top of the normal button.
[assembly: ExportRenderer(typeof(MyAdvantagePage), typeof
(MyAdvantagePageRenderer))]
namespace Advantage.MyAdvantage.MobileApp.iOS.Renderers
{
public class MyAdvantagePageRenderer : Xamarin.Forms.Platform.iOS.PageRenderer
{
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (((MyAdvantagePage)Element).EnableBackButtonOverride)
{
SetCustomBackButton();
}
}
private void SetCustomBackButton()
{
UIButton btn = new UIButton();
btn.Frame = new CGRect(0, 0, 50, 40);
btn.BackgroundColor = UIColor.Clear;
btn.TouchDown += (sender, e) =>
{
// Whatever your custom back button click handling
if (((MyAdvantagePage)Element)?.
CustomBackButtonAction != null)
{
((MyAdvantagePage)Element)?.
CustomBackButtonAction.Invoke();
}
};
NavigationController.NavigationBar.AddSubview(btn);
}
}
}
Android, is tricky. In older versions and future versions of Forms once fixed, you can simply override the OnOptionsItemselected like this
public override bool OnOptionsItemSelected(IMenuItem item)
{
// check if the current item id
// is equals to the back button id
if (item.ItemId == 16908332)
{
// retrieve the current xamarin forms page instance
var currentpage = (MyAdvantagePage)
Xamarin.Forms.Application.
Current.MainPage.Navigation.
NavigationStack.LastOrDefault();
// check if the page has subscribed to
// the custom back button event
if (currentpage?.CustomBackButtonAction != null)
{
// invoke the Custom back button action
currentpage?.CustomBackButtonAction.Invoke();
// and disable the default back button action
return false;
}
// if its not subscribed then go ahead
// with the default back button action
return base.OnOptionsItemSelected(item);
}
else
{
// since its not the back button
//click, pass the event to the base
return base.OnOptionsItemSelected(item);
}
}
However, if you are using FormsAppCompatActivity, then you need to add onto your OnCreate in MainActivity this to set your toolbar:
Android.Support.V7.Widget.Toolbar toolbar = this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
But wait! If you have too old a version of .Forms or too new version, a bug will come up where toolbar is null. If this happens, the hacked together way I got it to work to make a deadline is like this. In OnCreate in MainActivity:
MobileApp.Pages.Articles.ArticleDetail.androdAction = () =>
{
Android.Support.V7.Widget.Toolbar toolbar = this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
};
ArticleDetail is a Page, and androidAction is an Action that I run on OnAppearing if the Platform is Android on my page. By this point in your app, toolbar will no longer be null.
Couple more steps, the iOS render we made above uses properties that you need to add to whatever page you are making the renderer for. I was making it for my MyAdvantagePage class that I made, which implements ContentPage . So in my MyAdvantagePage class I added
public Action CustomBackButtonAction { get; set; }
public static readonly BindableProperty EnableBackButtonOverrideProperty =
BindableProperty.Create(
nameof(EnableBackButtonOverride),
typeof(bool),
typeof(MyAdvantagePage),
false);
/// <summary>
/// Gets or Sets Custom Back button overriding state
/// </summary>
public bool EnableBackButtonOverride
{
get
{
return (bool)GetValue(EnableBackButtonOverrideProperty);
}
set
{
SetValue(EnableBackButtonOverrideProperty, value);
}
}
Now that that is all done, on any of my MyAdvantagePage I can add this
:
this.EnableBackButtonOverride = true;
this.CustomBackButtonAction = async () =>
{
if (_browser.CanGoBack)
{
_browser.GoBack();
}
else
{
await Navigation.PopAsync(true);
}
};
That should be everything to get it to work on Android hardware back, and navigation back for both android and iOS.
You are right, in your page class override OnBackButtonPressed and return true if you want to prevent navigation. It works fine for me and I have the same version.
protected override bool OnBackButtonPressed()
{
if (Condition)
return true;
return base.OnBackButtonPressed();
}
Depending on what exactly you are looking for (I would not recommend using this if you simply want to cancel back button navigation), OnDisappearing may be another option:
protected override void OnDisappearing()
{
//back button logic here
}
OnBackButtonPressed() this will be called when a hardware back button is pressed as in android. This will not work on the software back button press as in ios.
Additional to Kyle Answer
Set
Inside YOURPAGE
public static Action SetToolbar;
YOURPAGE OnAppearing
if (Device.RuntimePlatform == Device.Android)
{
SetToolbar.Invoke();
}
MainActivity
YOURPAGE.SetToolbar = () =>
{
Android.Support.V7.Widget.Toolbar toolbar =
this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
};
I use Prism libray and for handle the back button/action I extend INavigatedAware interface of Prism on my page and I implement this methods:
public void OnNavigatedFrom(INavigationParameters parameters)
{
if (parameters.GetNavigationMode() == NavigationMode.Back)
{
//Your code
}
}
public void OnNavigatedTo(INavigationParameters parameters)
{
}
Method OnNavigatedFrom is raised when user press back button from Navigation Bar (Android & iOS) and when user press Hardware back button (only for Android).
For anyone still fighting with this issue - basically you cannot intercept back navigation cross-platform. Having said that there are two approaches that effectively solve the problem:
Hide the NavigationPage back button with NavigationPage.ShowHasBackButton(this, false) and push a modal page that has a custom Back/Cancel/Close button
Intercept the back navigation natively for each platform. This is a good article that does it for iOS and Android: https://theconfuzedsourcecode.wordpress.com/2017/03/12/lets-override-navigation-bar-back-button-click-in-xamarin-forms/
For UWP you are on your own :)
Edit:
Well, not anymore since I did it :) It actually turned out to be pretty easy – there is just one back button and it’s supported by Forms so you just have to override ContentPage’s OnBackButtonPressed:
protected override bool OnBackButtonPressed()
{
if (Device.RuntimePlatform.Equals(Device.UWP))
{
OnClosePageRequested();
return true;
}
else
{
base.OnBackButtonPressed();
return false;
}
}
async void OnClosePageRequested()
{
var tdvm = (TaskDetailsViewModel)BindingContext;
if (tdvm.CanSaveTask())
{
var result = await DisplayAlert("Wait", "You have unsaved changes! Are you sure you want to go back?", "Discard changes", "Cancel");
if (result)
{
tdvm.DiscardChanges();
await Navigation.PopAsync(true);
}
}
else
{
await Navigation.PopAsync(true);
}
}
protected override bool OnBackButtonPressed()
{
base.OnBackButtonPressed();
return true;
}
base.OnBackButtonPressed() returns false on click of hardware back button.
In order to prevent operation of back button or prevent navigation to previous page. the overriding function should be returned as true. On return true, it stays on the current xamarin form page and state of page is also maintained.
The trick is to implement your own navigation page that inherits from NavigationPage. It has the appropriate events Pushed, Popped and PoppedToRoot.
A sample implementation could look like this:
public class PageLifetimeSupportingNavigationPage : NavigationPage
{
public PageLifetimeSupportingNavigationPage(Page content)
: base(content)
{
Init();
}
private void Init()
{
Pushed += (sender, e) => OpenPage(e.Page);
Popped += (sender, e) => ClosePage(e.Page);
PoppedToRoot += (sender, e) =>
{
var args = e as PoppedToRootEventArgs;
if (args == null)
return;
foreach (var page in args.PoppedPages.Reverse())
ClosePage(page);
};
}
private static void OpenPage(Page page)
{
if (page is IPageLifetime navpage)
navpage.OnOpening();
}
private static void ClosePage(Page page)
{
if (page is IPageLifetime navpage)
navpage.OnClosed();
page.BindingContext = null;
}
}
Pages would implement the following interface:
public interface IPageLifetime
{
void OnOpening();
void OnClosed();
}
This interface could be implemented in a base class for all pages and then delegate it's calls to it's view model.
The navigation page and could be created like this:
var navigationPage = new PageLifetimeSupportingNavigationPage(new MainPage());
MainPage would be the root page to show.
Of course you could also just use NavigationPage in the first place and subscribe to it's events without inheriting from it.
Maybe this can be usefull, You need to hide the back button, and then replace with your own button:
public static UIViewController AddBackButton(this UIViewController controller, EventHandler ev){
controller.NavigationItem.HidesBackButton = true;
var btn = new UIBarButtonItem(UIImage.FromFile("myIcon.png"), UIBarButtonItemStyle.Plain, ev);
UIBarButtonItem[] items = new[] { btn };
controller.NavigationItem.LeftBarButtonItems = items;
return controller;
}
public static UIViewController DeleteBack(this UIViewController controller)
{
controller.NavigationItem.LeftBarButtonItems = null;
return controller;
}
Then call them into these methods:
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
this.AddBackButton(DoSomething);
UpdateFrames();
}
public override void ViewWillDisappear(Boolean animated)
{
this.DeleteBackButton();
}
public void DoSomething(object sender, EventArgs e)
{
//Do a barrel roll
}
Another way around is to use Rg.Plugins.Popup Which allows you to implement nice popup. It uses another NavigationStack => Rg.Plugins.Popup.Services.PopupNavigation.Instance.PopupStack. So your page won't be wrap around the NavigationBar.
In your case I would simply
Create a full page popup with opaque background
Override ↩️ OnBackButtonPressed for Android on ⚠️ParentPage⚠️ with something like this:
protected override bool OnBackButtonPressed()
{
return Rg.Plugins.Popup.Services.PopupNavigation.Instance.PopupStack.Any();
}
Since the back-button affect the usual NavigationStack your parent would pop out whenever the user try to use it while your "popup is showing".
Now what? Xaml what ever you want to properly close your popup with all the check you want.
💥 Problem solved for these targets💥
[x] Android
[x] iOS
[-] Windows Phone (Obsolete. Use v1.1.0-pre5 if WP is needed)
[x] UWP (Min Target: 10.0.16299)

Resources