Xamarin.Forms Navigation with MVVM Light

MvvmLightNavigationTitle

I wrote this post a couple of years ago and while the implementation bellow worked for me in the beginning, I tended to reach it’s limitation rather quickly in larger apps. So I encourage you to check out an updated version of the Navigation Service in one of my later posts.

Working with Model View ViewModel (MVVM) really enables you to reuse as much code as possible between platforms. For getting started with MVVM Light and Xamarin.Forms make sure to checkout my post on the topic. In this post I would really like to dive into another topic of MVVM Light namely the navigation service. Navigating between pages is normally done on by invoking a platform dependent API call, calling these Interfaces in your View Model usually is though of as bad design as it tightly couples your View Model to the platform and therefore prevents sharing the VM across platforms. MVVM Light provides a Navigation Service that lets us invoke page navigations without coupling it to a platform.

Creating the application

With Xamarin.Forms you have two ways how to create your Application, Sharedand and Portable Class Library (PCL). I recommend using the PCL as it will allow to reuse the most amount of code when targeting other platforms such as .Net or Windows Store apps. When writing your UI with Xamarin.Forms you again have the option of writing your UI in C# aka code behind or use XAML to create your UIs. Again I choose the later as XAML provides a nice way of formatting your UI components and provides a natural way of interaction if you have a WPF, Silverlight or Windows Apps background.

The app will exist of three pages:

NavigationOverview

The first page contains a button which when selected navigates to the second page:

<?xml version="1.0" encoding="utf-8" ?><ContentPage xmlns="http://xamarin.com/schemas/2014/forms"             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"             x:Class="MvvmLightNavigation.XamarinForms.Views.FirstPage">  <Button Text="Navigate to second page" Command="{Binding NavigateCommand}" VerticalOptions="Center" HorizontalOptions="Center" /></ContentPage>

The second page has a Text box and a button, the text box will be sent to the third page as parameter and the button is again used to trigger the navigation.

<?xml version="1.0" encoding="utf-8" ?><ContentPage xmlns="http://xamarin.com/schemas/2014/forms"             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"             x:Class="MvvmLightNavigation.XamarinForms.Views.SecondPage">  <Entry Placeholder="Please enter a parameter" Text="{Binding Paramter}" VerticalOptions="Center" HorizontalOptions="Center" />  <Button Text="Navigate to second page" Command="{Binding NavigateCommand}" VerticalOptions="Center" HorizontalOptions="Center" /></ContentPage>

On the third page we have a label to display the navigation content. The button can be used instead of the back button (which on Android and Windows may be physical) to get back to the second page:

<?xml version="1.0" encoding="utf-8" ?><ContentPage xmlns="http://xamarin.com/schemas/2014/forms"             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"             x:Class="MvvmLightNavigation.XamarinForms.Views.ThirdPage">  <Label Text="{Binding ParameterText}" VerticalOptions="Center" HorizontalOptions="Center" />  <Button Text="Go back" Command="{Binding NavigateCommand}" VerticalOptions="Center" HorizontalOptions="Center" /></ContentPage>

Wiring up the navigation

First we will have setup an interface to the navigation stack of Xamarin.Forms, so lets write/copy&paste our NavigationService.cs class:

public class NavigationService : INavigationService{    private readonly Dictionary<string, Type> _pagesByKey = new Dictionary<string, Type>();    private NavigationPage _navigation;    public string CurrentPageKey    {        get        {            lock (_pagesByKey)            {                if (_navigation.CurrentPage == null)                {                    return null;                }                var pageType = _navigation.CurrentPage.GetType();                return _pagesByKey.ContainsValue(pageType)                    ? _pagesByKey.First(p => p.Value == pageType).Key                    : null;            }        }    }    public void GoBack()    {        _navigation.PopAsync();    }    public void NavigateTo(string pageKey)    {        NavigateTo(pageKey, null);    }    public void NavigateTo(string pageKey, object parameter)    {        lock (_pagesByKey)        {            if (_pagesByKey.ContainsKey(pageKey))            {                var type = _pagesByKey[pageKey];                ConstructorInfo constructor;                object[] parameters;                if (parameter == null)                {                    constructor = type.GetTypeInfo()                        .DeclaredConstructors                        .FirstOrDefault(c => !c.GetParameters().Any());                    parameters = new object[]                    {                    };                }                else                {                    constructor = type.GetTypeInfo()                        .DeclaredConstructors                        .FirstOrDefault(                            c =>                            {                                var p = c.GetParameters();                                return p.Count() == 1                                       && p[0].ParameterType == parameter.GetType();                            });                    parameters = new[]                    {                        parameter                    };                }                if (constructor == null)                {                    throw new InvalidOperationException(                        "No suitable constructor found for page " + pageKey);                }                var page = constructor.Invoke(parameters) as Page;                _navigation.PushAsync(page);            }            else            {                throw new ArgumentException(                    string.Format(                        "No such page: {0}. Did you forget to call NavigationService.Configure?",                        pageKey),                    "pageKey");            }        }    }    public void Configure(string pageKey, Type pageType)    {        lock (_pagesByKey)        {            if (_pagesByKey.ContainsKey(pageKey))            {                _pagesByKey[pageKey] = pageType;            }            else            {                _pagesByKey.Add(pageKey, pageType);            }        }    }    public void Initialize(NavigationPage navigation)    {        _navigation = navigation;    }}

This code was written by Laurent Bugnion for his 2014 Evolve talk. To use the navigation service the pages have to be registered as key value pairs where the key is a representative String and the value is the page itself, this is done in the Locator.cs class:

public const string FirstPage = "FirstPage";public const string SecondPage = "SecondPage";public const string ThirdPage = "ThirdPage";

Making the Keys public will ensure that other classes i.e. the ViewModels will be able to invoke the Navigation Servicewithout running the risk breaking navigation when Keys are getting renamed due to refactoring. Next setup the navigation service in the App.cs class:

public class App : Application{    // ...    public App()    {        var nav = new NavigationService();        nav.Configure(Locator.FirstPage, typeof(FirstPage));        nav.Configure(Locator.SecondPage, typeof(SecondPage));        nav.Configure(Locator.ThirdPage, typeof(ThirdPage));        SimpleIoc.Default.Register<INavigationService>(() => nav);        var firstPage = new NavigationPage(new FirstPage());        nav.Initialize(firstPage);        //SimpleIoc.Default.Register<INavigationService>(() => nav);        MainPage = firstPage;    }    // ...}

As you can see it’s simply matching the keys with the representing page.

The ViewModels

We could leave out the ViewModels and just invoke the navigation commands on the event handler in the code behind, but in most scenarios you will invoke the navigation from within the ViewModel as it will allow you to reuse the code even when you migrate from a Xamarin.Forms app to a Windows Store, Xamarin.iOS or Xamarin.Android stack. So lets create a View Model for each page.

The first ViewModel exists mainly out of the RelayCommand:

private readonly INavigationService _navigationService;public FirstViewModel(INavigationService navigationService){    if (navigationService == null) throw new ArgumentNullException("navigationService");    _navigationService = navigationService;    NavigateCommand = new RelayCommand(() => { _navigationService.NavigateTo(Locator.SecondPage); });}public ICommand NavigateCommand { get; set; }

When the relay command is invoked the navigation is triggered and a push navigation will be executed. The second ViewModel offers a property that can be set and used as navigation parameter:

private readonly INavigationService _navigationService;public SecondViewModel(INavigationService navigationService){    if (navigationService == null) throw new ArgumentNullException("navigationService");    _navigationService = navigationService;    NavigationCommand =        new RelayCommand(() => { _navigationService.NavigateTo(Locator.ThirdPage, Parameter ?? string.Empty); });}public ICommand NavigationCommand { get; set; }public string Parameter { get; set; }

Passing a parameter is done easily as you can see. Note that if you pass null to the navigation service the default constructor will be invoked, which may not be what you intend to do. Receiving the parameter is done via the constructor on the code behind of the third page:

public partial class ThirdPage : ContentPage{    public ThirdPage(string parameter)    {        InitializeComponent();        var viewModel = App.Locator.Third;        BindingContext = viewModel;        viewModel.ParameterText = string.IsNullOrEmpty(parameter) ? "No parameter set" : parameter;    }}

Within the constructor the parameter is passed to the ViewModel. The third ViewModel is very similar to the second ViewModel:

private readonly INavigationService _navigationService;private string _parameterText;public ThirdViewModel(INavigationService navigationService){    if (navigationService == null) throw new ArgumentNullException("navigationService");    _navigationService = navigationService;    NavigateCommand = new RelayCommand(() => { _navigationService.GoBack(); });}public string ParameterText{    get { return _parameterText; }    set    {        if (_parameterText == value) return;        _parameterText = value;        RaisePropertyChanged(() => ParameterText);    }}public ICommand NavigateCommand { get; set; }

The back navigation is also implemented within the command. You can either select the button or simply use the default navigation buttons according to the system your app is running on.

Retrieving the current PageKey

This function can come in quite handy if you need to know which page is currently displayed and inform your ViewModel accordingly. By overriding the OnAppearing method in the code behind of the SecondPage.xaml.csyou see how the information can be read out:

protected override void OnAppearing(){    base.OnAppearing();    var currentPageKeyString = ServiceLocator.Current        .GetInstance<INavigationService>()        .CurrentPageKey;    Debug.WriteLine("Current page key: " + currentPageKeyString);}

Now every time you navigate to or back to the second page the information will be printed to the debug console.

Conclusion

This post shows how you can use MVVM Lights navigation service to navigate between pages, passing parameters and getting the information on which page is currently being displayed to the user. Using the Navigation Service from MVVM Light provides a nice solution for abstracting the navigation layer within your Xamarin.Forms application and allows you to reuse the code if you decide to use the more powerful Xamarin.iOS, Xamarin.Android or Windows native UI pages.

You can find the entire code on GitHub.

References

http://www.mvvmlight.net/doc/nav1.cshtml

Technorati Tags: Xamarin,Xamarin.Forms,MVVM Light,MVVM

Updated: