How to handle/cancel back navigation in Xamarin Forms - xamarin

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)

Related

How to handle Xamarin Webview where a link opens a new tab?

I'm using a Xamarin Webview to display a website which has buttons with linked pdf files. Browsers will open the pdf file in a new tab but webview doesn't support tabs and just does nothing on click of the button. How can I handle this?
The most preferable solution would be to show the new tab in the same webview, just as if it wouldn't open a new tab. I tried to implement a custom renderer which inherits from android webview, but even there I found no possibility to handle it or to just get the URL where the button redirects to.
Thanks in advance for your help.
WebView does not support tabs if you want a tabbed browser UI you need to implement it yourself.
1.Setting SetSupportMultipleWindows to true
2.Implementing WebChromeClient and override it's OnCreateWindow method.
public class MyWebViewRenderer : WebViewRenderer
{
public MyWebViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if(Control!=null)
{
Control.Settings.SetSupportMultipleWindows(true);
Control.Settings.JavaScriptEnabled = true;
Control.SetWebChromeClient(new MyWebChromeClient());
}
}
}
public class MyWebChromeClient: WebChromeClient
{
public override bool OnCreateWindow(Android.Webkit.WebView view, bool isDialog, bool isUserGesture, Message resultMsg)
{
if(!isDialog)
{
return true;
}
return base.OnCreateWindow(view, isDialog, isUserGesture, resultMsg);
}
}
}

Xamarin dismiss keyboard

So I have a searchbar(My searchbar all have a custom renderer) on one page. When a user fills in their search term they get redirect to searchpage where their results get shown.
The problem I have here is that suddenly the searchbar on the new page gets focussed (because I pass the search term from the previous page into this searchbar) and the software keyboard shows.
I want to dismiss keyboard or prevent the keyboard from showing. But when the user clicks inside the searchbar than ofcourse the keyboard can appear.
Note this question has been asked before, the answers I followed were never successfull. Here you can find what I have tried
First try: Unfocus
Just unfocussing my entry, did not work. Tried it both in my constructor and in my OnAppearing code of the page
Second try: Focus other element
Tried focussing my listview, but keyboard stil showed up
Third try: Dependency service
https://forums.xamarin.com/discussion/comment/172077#Comment_172077
Interface:
public interface IKeyboardHelper
{
void HideKeyboard();
}
iOS:
public class iOSKeyboardHelper : IKeyboardHelper
{
public void HideKeyboard()
{
UIApplication.SharedApplication.KeyWindow.EndEditing(true);
}
}
Droid:
public class DroidKeyboardHelper : IKeyboardHelper
{
public void HideKeyboard()
{
var context = Forms.Context;
var inputMethodManager = context.GetSystemService(Context.InputMethodService) as InputMethodManager;
if (inputMethodManager != null && context is Activity)
{
var activity = context as Activity;
var token = activity.CurrentFocus?.WindowToken;
inputMethodManager.HideSoftInputFromWindow(token, HideSoftInputFlags.None);
activity.Window.DecorView.ClearFocus();
}
}
}
Usage in Xamarin Forms:
DependencyService.Get<IKeyboardHelper>().HideKeyboard();
EDIT
My renderer's code for my search page
protected override void OnElementChanged(ElementChangedEventArgs<SearchBar> e)
{
base.OnElementChanged(e);
if (Control != null)
{
var searchView = Control;
searchView.Iconified = true;
searchView.SetIconifiedByDefault(false);
// (Resource.Id.search_mag_icon); is wrong / Xammie bug
int searchIconId = Context.Resources.GetIdentifier("android:id/search_mag_icon", null, null);
var icon = searchView.FindViewById(searchIconId);
(icon as ImageView).SetImageResource(Resource.Drawable.search_zwart);
int cancelIconId = Context.Resources.GetIdentifier("android:id/search_close_btn", null, null);
var eicon = searchView.FindViewById(cancelIconId);
(eicon as ImageView).SetImageResource(Resource.Drawable.close_zwart);
}
}

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 do you switch pages in Xamarin.Forms?

How do you switch between pages in Xamarin Forms?
My main page is a ContentPage and I don't want to switch to something like a Tabbed Page.
I've been able to pseudo-do it by finding parents of the controls that should trigger the new page until I find the ContentPage and then swap out the Content with controls for a new page. But this seems really sloppy.
In the App class you can set the MainPage to a Navigation Page and set the root page to your ContentPage:
public App ()
{
// The root page of your application
MainPage = new NavigationPage( new FirstContentPage() );
}
Then in your first ContentPage call:
Navigation.PushAsync (new SecondContentPage ());
Xamarin.Forms supports multiple navigation hosts built-in:
NavigationPage, where the next page slide in,
TabbedPage, the one you don't like
CarouselPage, that allows for switching left and right to next/prev pages.
On top of this, all pages also supports PushModalAsync() which just push a new page on top of the existing one.
At the very end, if you want to make sure the user can't get back to the previous page (using a gesture or the back hardware button), you can keep the same Page displayed and replace its Content.
The suggested options of replacing the root page works as well, but you'll have to handle that differently for each platform.
If your project has been set up as a PCL forms project (and very likely as Shared Forms as well but I haven't tried that) there is a class App.cs that looks like this:
public class App
{
public static Page GetMainPage ()
{
AuditorDB.Model.Extensions.AutoTimestamp = true;
return new NavigationPage (new LoginPage ());
}
}
you can modify the GetMainPage method to return a new TabbedPaged or some other page you have defined in the project
From there on you can add commands or event handlers to execute code and do
// to show OtherPage and be able to go back
Navigation.PushAsync(new OtherPage());
// to show AnotherPage and not have a Back button
Navigation.PushModalAsync(new AnotherPage());
// to go back one step on the navigation stack
Navigation.PopAsync();
Push a new page onto the stack, then remove the current page. This results in a switch.
item.Tapped += async (sender, e) => {
await Navigation.PushAsync (new SecondPage ());
Navigation.RemovePage(this);
};
You need to be in a Navigation Page first:
MainPage = NavigationPage(new FirstPage());
Switching content isn't ideal as you have just one big page and one set of page events like OnAppearing ect.
If you do not want to go the previous page i.e. do not let the user go back to the login screen once authorization is done, then you can use;
App.Current.MainPage = new HomePage();
If you want to enable back functionality, just use
Navigation.PushModalAsync(new HomePage())
Seems like this thread is very popular and it will be sad not to mention here that there is an alternative way - ViewModel First Navigation. Most of the MVVM frameworks out there using it, however if you want to understand what it is about, continue reading.
All the official Xamarin.Forms documentation is demonstrating a simple, yet slightly not MVVM pure solution. That is because the Page(View) should know nothing about the ViewModel and vice versa. Here is a great example of this violation:
// C# version
public partial class MyPage : ContentPage
{
public MyPage()
{
InitializeComponent();
// Violation
this.BindingContext = new MyViewModel();
}
}
// XAML version
<?xml version="1.0" encoding="utf-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:MyApp.ViewModel"
x:Class="MyApp.Views.MyPage">
<ContentPage.BindingContext>
<!-- Violation -->
<viewmodels:MyViewModel />
</ContentPage.BindingContext>
</ContentPage>
If you have a 2 pages application this approach might be good for you. However if you are working on a big enterprise solution you better go with a ViewModel First Navigation approach. It is slightly more complicated but much cleaner approach that allow you to navigate between ViewModels instead of navigation between Pages(Views). One of the advantages beside clear separation of concerns is that you could easily pass parameters to the next ViewModel or execute an async initialization code right after navigation. Now to details.
(I will try to simplify all the code examples as much as possible).
1. First of all we need a place where we could register all our objects and optionally define their lifetime. For this matter we can use an IOC container, you can choose one yourself. In this example I will use Autofac(it is one of the fastest available). We can keep a reference to it in the App so it will be available globally (not a good idea, but needed for simplification):
public class DependencyResolver
{
static IContainer container;
public DependencyResolver(params Module[] modules)
{
var builder = new ContainerBuilder();
if (modules != null)
foreach (var module in modules)
builder.RegisterModule(module);
container = builder.Build();
}
public T Resolve<T>() => container.Resolve<T>();
public object Resolve(Type type) => container.Resolve(type);
}
public partial class App : Application
{
public DependencyResolver DependencyResolver { get; }
// Pass here platform specific dependencies
public App(Module platformIocModule)
{
InitializeComponent();
DependencyResolver = new DependencyResolver(platformIocModule, new IocModule());
MainPage = new WelcomeView();
}
/* The rest of the code ... */
}
2.We will need an object responsible for retrieving a Page (View) for a specific ViewModel and vice versa. The second case might be useful in case of setting the root/main page of the app. For that we should agree on a simple convention that all the ViewModels should be in ViewModels directory and Pages(Views) should be in the Views directory. In other words ViewModels should live in [MyApp].ViewModels namespace and Pages(Views) in [MyApp].Views namespace. In addition to that we should agree that WelcomeView(Page) should have a WelcomeViewModel and etc. Here is a code example of a mapper:
public class TypeMapperService
{
public Type MapViewModelToView(Type viewModelType)
{
var viewName = viewModelType.FullName.Replace("Model", string.Empty);
var viewAssemblyName = GetTypeAssemblyName(viewModelType);
var viewTypeName = GenerateTypeName("{0}, {1}", viewName, viewAssemblyName);
return Type.GetType(viewTypeName);
}
public Type MapViewToViewModel(Type viewType)
{
var viewModelName = viewType.FullName.Replace(".Views.", ".ViewModels.");
var viewModelAssemblyName = GetTypeAssemblyName(viewType);
var viewTypeModelName = GenerateTypeName("{0}Model, {1}", viewModelName, viewModelAssemblyName);
return Type.GetType(viewTypeModelName);
}
string GetTypeAssemblyName(Type type) => type.GetTypeInfo().Assembly.FullName;
string GenerateTypeName(string format, string typeName, string assemblyName) =>
string.Format(CultureInfo.InvariantCulture, format, typeName, assemblyName);
}
3.For the case of setting a root page we will need sort of ViewModelLocator that will set the BindingContext automatically:
public static class ViewModelLocator
{
public static readonly BindableProperty AutoWireViewModelProperty =
BindableProperty.CreateAttached("AutoWireViewModel", typeof(bool), typeof(ViewModelLocator), default(bool), propertyChanged: OnAutoWireViewModelChanged);
public static bool GetAutoWireViewModel(BindableObject bindable) =>
(bool)bindable.GetValue(AutoWireViewModelProperty);
public static void SetAutoWireViewModel(BindableObject bindable, bool value) =>
bindable.SetValue(AutoWireViewModelProperty, value);
static ITypeMapperService mapper = (Application.Current as App).DependencyResolver.Resolve<ITypeMapperService>();
static void OnAutoWireViewModelChanged(BindableObject bindable, object oldValue, object newValue)
{
var view = bindable as Element;
var viewType = view.GetType();
var viewModelType = mapper.MapViewToViewModel(viewType);
var viewModel = (Application.Current as App).DependencyResolver.Resolve(viewModelType);
view.BindingContext = viewModel;
}
}
// Usage example
<?xml version="1.0" encoding="utf-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:MyApp.ViewModel"
viewmodels:ViewModelLocator.AutoWireViewModel="true"
x:Class="MyApp.Views.MyPage">
</ContentPage>
4.Finally we will need a NavigationService that will support ViewModel First Navigation approach:
public class NavigationService
{
TypeMapperService mapperService { get; }
public NavigationService(TypeMapperService mapperService)
{
this.mapperService = mapperService;
}
protected Page CreatePage(Type viewModelType)
{
Type pageType = mapperService.MapViewModelToView(viewModelType);
if (pageType == null)
{
throw new Exception($"Cannot locate page type for {viewModelType}");
}
return Activator.CreateInstance(pageType) as Page;
}
protected Page GetCurrentPage()
{
var mainPage = Application.Current.MainPage;
if (mainPage is MasterDetailPage)
{
return ((MasterDetailPage)mainPage).Detail;
}
// TabbedPage : MultiPage<Page>
// CarouselPage : MultiPage<ContentPage>
if (mainPage is TabbedPage || mainPage is CarouselPage)
{
return ((MultiPage<Page>)mainPage).CurrentPage;
}
return mainPage;
}
public Task PushAsync(Page page, bool animated = true)
{
var navigationPage = Application.Current.MainPage as NavigationPage;
return navigationPage.PushAsync(page, animated);
}
public Task PopAsync(bool animated = true)
{
var mainPage = Application.Current.MainPage as NavigationPage;
return mainPage.Navigation.PopAsync(animated);
}
public Task PushModalAsync<TViewModel>(object parameter = null, bool animated = true) where TViewModel : BaseViewModel =>
InternalPushModalAsync(typeof(TViewModel), animated, parameter);
public Task PopModalAsync(bool animated = true)
{
var mainPage = GetCurrentPage();
if (mainPage != null)
return mainPage.Navigation.PopModalAsync(animated);
throw new Exception("Current page is null.");
}
async Task InternalPushModalAsync(Type viewModelType, bool animated, object parameter)
{
var page = CreatePage(viewModelType);
var currentNavigationPage = GetCurrentPage();
if (currentNavigationPage != null)
{
await currentNavigationPage.Navigation.PushModalAsync(page, animated);
}
else
{
throw new Exception("Current page is null.");
}
await (page.BindingContext as BaseViewModel).InitializeAsync(parameter);
}
}
As you may see there is a BaseViewModel - abstract base class for all the ViewModels where you can define methods like InitializeAsync that will get executed right after the navigation. And here is an example of navigation:
public class WelcomeViewModel : BaseViewModel
{
public ICommand NewGameCmd { get; }
public ICommand TopScoreCmd { get; }
public ICommand AboutCmd { get; }
public WelcomeViewModel(INavigationService navigation) : base(navigation)
{
NewGameCmd = new Command(async () => await Navigation.PushModalAsync<GameViewModel>());
TopScoreCmd = new Command(async () => await navigation.PushModalAsync<TopScoreViewModel>());
AboutCmd = new Command(async () => await navigation.PushModalAsync<AboutViewModel>());
}
}
As you understand this approach is more complicated, harder to debug and might be confusing. However there are many advantages plus you actually don't have to implement it yourself since most of the MVVM frameworks support it out of the box. The code example that is demonstrated here is available on github. There are plenty of good articles about ViewModel First Navigation approach and there is a free Enterprise Application Patterns using Xamarin.Forms eBook which is explaining this and many other interesting topics in detail.
By using the PushAsync() method you can push and PopModalAsync() you can pop pages to and from the navigation stack. In my code example below I have a Navigation page (Root Page) and from this page I push a content page that is a login page once I am complete with my login page I pop back to the root page
~~~ Navigation can be thought of as a last-in, first-out stack of Page objects.To move from one page to another an application will push a new page onto this stack. To return back to the previous page the application will pop the current page from the stack. This navigation in Xamarin.Forms is handled by the INavigation interface
Xamarin.Forms has a NavigationPage class that implements this interface and will manage the stack of Pages. The NavigationPage class will also add a navigation bar to the top of the screen that displays a title and will also have a platform appropriate Back button that will return to the previous page. The following code shows how to wrap a NavigationPage around the first page in an application:
Reference to content listed above and a link you should review for more information on Xamarin Forms, see the Navigation section:
http://developer.xamarin.com/guides/cross-platform/xamarin-forms/introduction-to-xamarin-forms/
~~~
public class MainActivity : AndroidActivity
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
Xamarin.Forms.Forms.Init(this, bundle);
// Set our view from the "main" layout resource
SetPage(BuildView());
}
static Page BuildView()
{
var mainNav = new NavigationPage(new RootPage());
return mainNav;
}
}
public class RootPage : ContentPage
{
async void ShowLoginDialog()
{
var page = new LoginPage();
await Navigation.PushModalAsync(page);
}
}
//Removed code for simplicity only the pop is displayed
private async void AuthenticationResult(bool isValid)
{
await navigation.PopModalAsync();
}
In App.Xaml.Cs:
MainPage = new NavigationPage( new YourPage());
When you wish to navigate from YourPage to the next page you do:
await Navigation.PushAsync(new YourSecondPage());
You can read more about Xamarin Forms navigation here: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/navigation/hierarchical
Microsoft has quite good docs on this.
There is also the newer concept of the Shell. It allows for a new way of structuring your application and simplifies navigation in some cases.
Intro: https://devblogs.microsoft.com/xamarin/shell-xamarin-forms-4-0-getting-started/
Video on basics of Shell: https://www.youtube.com/watch?v=0y1bUAcOjZY&t=3112s
Docs: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/
Call:
((App)App.Current).ChangeScreen(new Map());
Create this method inside App.xaml.cs:
public void ChangeScreen(Page page)
{
MainPage = page;
}
In Xamarin we have page called NavigationPage. It holds stack of ContentPages.
NavigationPage has method like PushAsync() and PopAsync(). PushAsync add a page at the top of the stack, at that time that page will become the currently active page. PopAsync() method remove the page from the top of the stack.
In App.Xaml.Cs set like.
MainPage = new NavigationPage( new YourPage());
From YourPage you await Navigation.PushAsync(new newPage()); this method will add newPage at the top of the stack. At this time newPage will be currently active page.
One page to another page navigation in Xamarin.forms using Navigation property Below sample code
void addClicked(object sender, EventArgs e)
{
//var createEmp = (Employee)BindingContext;
Employee emp = new Employee();
emp.Address = AddressEntry.Text;
App.Database.SaveItem(emp);
this.Navigation.PushAsync(new EmployeeDetails());
this.Navigation.PushModalAsync(new EmployeeDetails());
}
To navigate one page to another page with in view cell Below code Xamrian.forms
private async void BtnEdit_Clicked1(object sender, EventArgs e)
{
App.Database.GetItem(empid);
await App.Current.MainPage.Navigation.PushModalAsync(new EmployeeRegistration(empid));
}
Example like below
public class OptionsViewCell : ViewCell
{
int empid;
Button btnEdit;
public OptionsViewCell()
{
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if (this.BindingContext == null)
return;
dynamic obj = BindingContext;
empid = Convert.ToInt32(obj.Eid);
var lblname = new Label
{
BackgroundColor = Color.Lime,
Text = obj.Ename,
};
var lblAddress = new Label
{
BackgroundColor = Color.Yellow,
Text = obj.Address,
};
var lblphonenumber = new Label
{
BackgroundColor = Color.Pink,
Text = obj.phonenumber,
};
var lblemail = new Label
{
BackgroundColor = Color.Purple,
Text = obj.email,
};
var lbleid = new Label
{
BackgroundColor = Color.Silver,
Text = (empid).ToString(),
};
//var lbleid = new Label
//{
// BackgroundColor = Color.Silver,
// // HorizontalOptions = LayoutOptions.CenterAndExpand
//};
//lbleid.SetBinding(Label.TextProperty, "Eid");
Button btnDelete = new Button
{
BackgroundColor = Color.Gray,
Text = "Delete",
//WidthRequest = 15,
//HeightRequest = 20,
TextColor = Color.Red,
HorizontalOptions = LayoutOptions.EndAndExpand,
};
btnDelete.Clicked += BtnDelete_Clicked;
//btnDelete.PropertyChanged += BtnDelete_PropertyChanged;
btnEdit = new Button
{
BackgroundColor = Color.Gray,
Text = "Edit",
TextColor = Color.Green,
};
// lbleid.SetBinding(Label.TextProperty, "Eid");
btnEdit.Clicked += BtnEdit_Clicked1; ;
//btnEdit.Clicked += async (s, e) =>{
// await App.Current.MainPage.Navigation.PushModalAsync(new EmployeeRegistration());
//};
View = new StackLayout()
{
Orientation = StackOrientation.Horizontal,
BackgroundColor = Color.White,
Children = { lbleid, lblname, lblAddress, lblemail, lblphonenumber, btnDelete, btnEdit },
};
}
private async void BtnEdit_Clicked1(object sender, EventArgs e)
{
App.Database.GetItem(empid);
await App.Current.MainPage.Navigation.PushModalAsync(new EmployeeRegistration(empid));
}
private void BtnDelete_Clicked(object sender, EventArgs e)
{
// var eid = Convert.ToInt32(empid);
// var item = (Xamarin.Forms.Button)sender;
int eid = empid;
App.Database.DeleteItem(empid);
}
}
After PushAsync use PopAsync (with this) to remove current page.
await Navigation.PushAsync(new YourSecondPage());
this.Navigation.PopAsync(this);
XAML page add this
<ContentPage.ToolbarItems>
<ToolbarItem Text="Next" Order="Primary"
Activated="Handle_Activated"/>
</ContentPage.ToolbarItems>
on the CS page
async void Handle_Activated(object sender, System.EventArgs e)
{
await App.Navigator.PushAsync(new PAGE());
}

Silverlight TabItem template not working correctly

In a SL4 application i need to restyle my TabItems (actually add a button in the header).
So i took the TabItem's control template from here and added the functionality i wanted.
This seems to work fine, (i could dynamically add tabitems) with one exception:
i think this posted control template is behaving somehow "arbitrary": every time the mouse hoovers over a non selected TabItem header, this gets selected WHITHOUT clicking!! (afaik this is not the default behavior: the user user has to click a header to make this tabitem the selected one).
I tried to find why it is behaving like this, with no luck!
Is there someone who can enlighten my darkness???
Thanks in advance!
Well it turns out the error was not in the control template but in the class, the style was applied to.
In detail: the class the style was applied to is the following (in it you will see my comment about the "wrong behavior"):
public class WorkspaceViewModel : TabItem
{
public WorkspaceViewModel()
{
DefaultStyleKey = typeof(WorkspaceViewModel);
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Button closeButtonSel = base.GetTemplateChild("PART_CloseTopSelected") as Button;
Button closeButtonUnsel = base.GetTemplateChild("PART_CloseTopUnSelected") as Button;
if (closeButtonSel != null)
closeButtonSel.Click += new RoutedEventHandler(closeButtonSel_Click);
if (closeButtonUnsel != null)
closeButtonUnsel.Click += new RoutedEventHandler(closeButtonSel_Click);
//this part is causing the effect i was complaining about!
//and has to be removed
this.MouseEnter += delegate(object sender, MouseEventArgs e)
{
IsSelected = true;
};
}
void closeButtonSel_Click(object sender, RoutedEventArgs e)
{
//this is the close request method used in the CloseTabItemCommand
OnRequestClose();
}
#region CloseTabItemCommand
private RelayCommand closeTabItemCommand;
public ICommand CloseTabItemCommand
{
get
{
if (this.closeTabItemCommand == null)
this.closeTabItemCommand = new RelayCommand(p => this.OnRequestClose(), p => this.CanCloseTabItem());
return this.closeTabItemCommand;
}
}
private bool CanCloseTabItem()
{
return true;
}
public event EventHandler RequestClose;
private void OnRequestClose()
{
if (RequestClose != null)
RequestClose(this, EventArgs.Empty);
}
#endregion
}

Resources