MVVM Navigation with Xamarin.Forms Shell

One of the topics that I find a bit contentious amongst app developers is how navigation is handled, specifically when it comes to MVVM. Should navigation be done in the code behind of the page? or in the ViewModel? and should navigation be based on a path/URL or based on what ViewModel you want to go to. Regardless of where you stand on this discussion, one thing is very clear – in your application, you need to set a standard mechanism for doing this. Prism and MvvmCross are quite prescriptive when it comes to navigation, making them great starting points for your application; Xamarin.Forms Shell attempts to provide some constructs (eg routes) but fails to deliver an entire strategy for handling navigation between all the pages within your application.

Cross-platform Navigation

Back when I started doing Windows Phone development navigation between pages was simply done by calling Navigate on the Frame of the application. iOS and Android had their own way of doing navigation, which was fine back then because cross-platform wasn’t a thing and each platform was typically built by itself. However, with the introduction of Xamarin (back then monoTouch and monoDroid) it was possible to reuse our C# code and so more thought was applied to how we could reuse not just business logic but also our UI logic. It was also about this time that the MVVM pattern was really catching on and there was time being invested by the community into establishing frameworks for associating viewmodels with views/pages.

I’ve been a big advocate for MVVM frameworks because I see them providing the much-needed pattern for instantiating viewmodels, handling dependency injection, and a consistent pattern for navigation that can be invoked from the viewmodel. I believe that this allows us to build viewmodels that are testable and aren’t interconnected with the UI due to the separation imposed by data binding. If you look back at the origins of MvvmCross you can see that it was developed with this in mind and supported iOS, Android and Windows without any additional UI frameworks (such as Xamarin.Forms). To this day, if you want to develop using Xamarin.iOS and Xamarin.Droid and you want to reuse your business logic, MvvmCross has you covered.

ViewModel to ViewModel Navigation

Coming back to the discussion around navigation, one of the concepts added to MvvmCross was the idea of viewmodel to viewmodel navigation. This meant you could navigate between views/pages by simply calling Navigate and providing the viewmodel you wanted to go to. MvvmCross would determine what view/page this corresponded to and would invoke the platform-specific method for navigation.

The abstraction of view/page navigation was one of the features that Xamarin.Forms brought to the table. In addition to having a common model for defining page layouts (ie XAML), there was now a single model for navigating between pages. Does this negate the need for say MvvmCross? Not at all because whilst Xamarin.Forms deals with the platform navigation idiosyncrasies, it doesn’t provide the abstraction that allows for testing. Whether you’re using Prism or MvvmCross, the INavigationService, gives you the ability to mock out navigation for testing purposes.

ViewModel Dependency Injection

We’re starting to get somewhere now – our navigation pattern needs to be platform agnostic (which we get from Xamarin.Forms) and needs to be testable. The last point I’ll make on MVVM frameworks is that both provide sophisticated dependency injection support. In order for our viewmodels to populate themselves with data, they’ll typically rely on other classes (eg service or data manager classes). Again, in order to make things testable, these services implement interfaces, and our viewmodels only depend (often as part of the constructor) on the interfaces. This requires some form of dependency injection in order to construct each viewmodel.

In summary, our requirements for navigation are:

  • Navigation needs to be platform agnostic
  • Viewmodels need to be testable
  • Viewmodels will have dependencies

This post isn’t here to talk up any particular framework; in fact, quite the reverse. We’re going to go framework-free and see where this leads us. One of the motivations here is the lack of support offered by MVVM frameworks for Xamarin.Forms Shell, which is still hampered by the Xamarin.Forms team (see this open issue). Given the above requirements for navigation, let’s see what we can come up with.

MVVM Basics with Xamarin.Forms Shell

I’m going to back-track to when I first used MVVM – Coming from building apps for PocketPC/Windows Mobile I viewed reflection as a luxury that desktop developers could afford, so a lot of what I built used concrete types with minimal code to wire things up. We’re going to start there and see where it leads us.

Note: This is a thought experiment ONLY – for commercial applications I would currently still be advocating the use of Prism or MvvmCross (rather than Shell).

ViewModelLocator

I’m going to start with a brand new Xamarin.Forms Shell application created in Visual Studio 2019 (16.3 Preview 2) using the Shell solution template. After creating the project the first thing to do is to check it runs; followed by upgrading NuGet packages; and then checking it still runs – there’s nothing like wasting hours wondering why your code doesn’t work, only to find out that a NuGet update broke something.

If we take a look at the AboutPage.xaml that’s created by the template we can see that the BindingContext is set to a new instance of the AboutViewModel. This addresses the basics of MVVM, where you have a viewmodel that is data bound to the page, thus making the viewmodel testable. However, it doesn’t solve the issue of how dependencies can be injected into the viewmodel.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             x:Class="FormsNaviation.Views.AboutPage"
             xmlns:vm="clr-namespace:FormsNaviation.ViewModels"
             Title="{Binding Title}">
    <ContentPage.BindingContext>
        <vm:AboutViewModel />
    </ContentPage.BindingContext>

To give us more control over how the viewmodels are created, we’re going to add in a class that will be responsible for constructing the viewmodels. The ViewModelLocator class will not only be responsible for constructing viewmodels, it will also be responsible for supplying any dependencies the viewmodels may rely on. Our first pass on the ViewModelLocator will be super basic.

public class ViewModelLocator
{
    public AboutViewModel About => new AboutViewModel();
}

To create an instance of the ViewModelLocator that can be referenced throughout the XAML of our application, we can add the instance to the App.xaml file as an Application resource.

<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:viewmodels="clr-namespace:FormsNaviation.ViewModels"
             x:Class="FormsNaviation.App">
    <Application.Resources>
        <viewmodels:ViewModelLocator x:Key="ViewModelLocator"/>
    </Application.Resources>
</Application>

And our AboutPage XAML is updated to reference the instance of the ViewModelLocator as a StaticResource.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
       x:Class="FormsNaviation.Views.AboutPage"
       xmlns:vm="clr-namespace:FormsNaviation.ViewModels"
       BindingContext="{Binding About, Source={StaticResource ViewModelLocator}}"
       x:DataType="vm:AboutViewModel" >

The AboutViewModel is returned using the About property exposed on the ViewModelLocator. In order to create viewmodels for other pages, you simply need to create a new property that returns the viewmodel (or it can return they are around the village.

At this stage, we haven’t really added much value over simply creating new instances of the viewmodel in the XAML on each page. However, as we get into dependencies, you’ll understand why it’s important to have at least some helper items that can assist with the development.

Adding Dependencies

As I mentioned, the use of a ViewModelLocator to create the instance of the viewmodel means that there’s a very clear point during the lifecycle to add and remove dependencies. The following locator code illustrates adding a dependency for the ILoginService, which in turn requires an implementation of the IAuthenticationService. I acknowledge that this is a very contrived example, especially since the verification check is just to compare username/password (clearly not a secure approach), but hopefully, it illustrates how dependencies can be provided and that they can be Lazy created at the point they’re first required.

public class ViewModelLocator
{
    private readonly Lazy<IAuthenticationService> authenticationService;
    private readonly Lazy<ILoginService> loginService;

    public ViewModelLocator()
    {
        authenticationService = new Lazy<IAuthenticationService>(() => new AuthenticationService());
        loginService = new Lazy<ILoginService>(() => new LoginService(authenticationService.Value));
    }

    public AboutViewModel About => new AboutViewModel(loginService.Value);

    public NewItemViewModel NewItem => new NewItemViewModel();
}

public interface ILoginService
{
    bool IsLoggedIn { get; }

    bool Login(string username, string password);
}
public class LoginService : ILoginService
{
    private bool isLoggedIn;
    public bool IsLoggedIn => isLoggedIn;

    private readonly IAuthenticationService authenticationService;
    public LoginService(IAuthenticationService authentication)
    {
        authenticationService = authentication;
    }

    public bool Login(string username, string password)
    {
        isLoggedIn = authenticationService.VerifyCredentials(username, password);
        return IsLoggedIn;
    }
}
public interface IAuthenticationService
{
    bool VerifyCredentials(string username, string password);
}
public class AuthenticationService : IAuthenticationService
{
    public bool VerifyCredentials(string username, string password)
    {
        return !string.IsNullOrWhiteSpace(username) && username == password;
    }
}

Going back to our original requirements, we now have the ability to provision viewmodels in a way that they are testable (i.e. they have no dependencies that can’t be mocked as part of testing the viewmodel) and that we can inject dependencies. The one thing we haven’t dealt with is the need to have a navigation pattern that is platform agnostic.

Event-Based Navigation

As we observed earlier Xamarin.Forms already provides the abstraction to for platform-agnostic navigation using the INavigation instance. So the issue we’re really left with is how to structure a navigation pattern that is consistent and easy to follow.

MvvmCross provides this in the form of ViewModel-to-ViewModel navigation but this introduces a dependency between viewmodels (and to the INavigationService), which actually makes it harder to test and introduces an interdependency between viewmodels, even if it is a loose dependency via navigation.

Rather than making individual viewmodels aware of navigation, what if we simply stipulate that the viewmodel should raise an event when it is “done”. The definition of done might be that the user has clicked the Ok or Cancel button (which would trigger a command on the viewmodel), or it could be that a new item has successfully been saved. The point is that the viewmodel raises one or more events when it wants to notify the application that the viewmodel has been completed, or “done”. At this point the application needs to make a decision on where to send the user (i.e. which page to navigate to).

What we want to avoid is the viewmodel raising an event that is captured by the corresponding page, in order to then navigate to the next page (or to go back to previous page in the case of a cancel style event). Whilst this keeps the viewmodel from taking on additional dependencies, this would result in navigation code being littered throughout the application, making it hard to find, update and maintain.

An alternative would be to have a central mechanism where navigation, based on viewmodel events, is declared. Remembering that this is a thought experiment only, and that this code would require significant refinement if it was to be used in production.

ViewModel Events

When a particular viewmodel raises an event, we want to be able to invoke an Action that will define how the application should respond. In concrete terms, we’re going to add a NewItemViewModel to the application generated from the Shell template (for some reason the NewItemPage doesn’t have its own viewmodel in the template). The NewItemViewModel will have a Cancel event which will be triggered if the user wants to abandon the process of creating a new item.

public class NewItemViewModel : BaseViewModel
{
    public event EventHandler<Item> ItemSaved;
    public event EventHandler Cancel;

    private Item item;
    public Item Item { get => item; set => SetProperty(ref item, value); }

    public ICommand SaveItemCommand { get; }
    public ICommand CancelCommand { get; }

    public NewItemViewModel()
    {
        SaveItemCommand = new Command(() =>
            {
                MessagingCenter.Send(this, "AddItem", Item);
                ItemSaved?.Invoke(this, Item);
            });
        CancelCommand = new Command(() =>
            {
                Cancel?.Invoke(this, EventArgs.Empty);
            });
    }
}

The application needs to intercept the Cancel event from the NewItemViewModel and navigate the application back to the previous page in the application. Normally an event handler would be wired up like the following code example, which you could expect to see in the OnAppearing method of a page (and then hopefully unwired in the OnDisappearing method).

vm.Cancel += (s, args) => Navigation.PopModalAsync();

However, we need to be able to abstract this code in a way that we can define what Action is invoked when a particular event is raised.

AppShell Event Maps

Let me jump forward a bit to where this is going to end up, so that you can see how we can declare the mapping between a viewmodel event and the Action that should be invoked.

public partial class AppShell : Xamarin.Forms.Shell
{
    public static IDictionary<Type, IEventMap[]> Maps { get; } = new Dictionary<Type, IEventMap[]>();

    public AppShell()
    {
        InitializeComponent();

        // When the ItemSaved event is raised on the NewItemViewModel
        // pop the current page off the navigation stack
        Maps.For<NewItemViewModel>()
            .Do(new EventHandler<Item>((s, args) => Navigation.PopModalAsync()))
            .When((vm, a) => vm.ItemSaved += a, (vm, a) => vm.ItemSaved -= a);

        // When the Cancel event is raised on the NewItemViewModel
        // pop the current page off the navigation stack
        Maps.For<NewItemViewModel>()
            .Do(new EventHandler((s, args) => Navigation.PopModalAsync()))
            .When((vm, a) => vm.Cancel += a, (vm, a) => vm.Cancel -= a);

        // When the SelectedItemChanged event is raised on the ItemsViewModel
        // navigate to the ItemDetailPage, passing in a new ItemDetailViewModel
        // with the selected item
        Maps.For<ItemsViewModel>()
            .Do(new EventHandler<Item>(async (s, args) =>
            {
                if (args == null)
                {
                    return;
                }
                await Navigation.PushAsync(new ItemDetailPage(new ItemDetailViewModel(args)));
            }))
            .When((vm, a) => vm.SelectedItemChanged += a, (vm, a) => vm.SelectedItemChanged -= a);
    }
}

Here’s what this code does:

  • I’ve define a dictionary of event to action mappings (i.e. Maps). We’ve declared as a static to make it easy to access throughout the app (disclaimer: this is a thought experiment!!!)
  • I’m using a fluent syntax, which I’ll include the code for later in this post, to link the type of viewmodel (For), the Action to be invoked (Do) and the Event (When).
  • The When method requires Actions for both wiring and unwiring the event handler due to limitations on C# events.

Connecting Event Mappings

Now that we have these mappings, we need to use them in the application itself. The mappings are used in the OnAppearing and OnDisappearing methods to wire and unwire the Action to the viewmodel events. To do this the pages in our application need to inherit from a base page that has the appropriate logic.

public class BasePage : ContentPage
{
    protected override void OnAppearing()
    {
        base.OnAppearing();

        var vm = this.BindingContext as BaseViewModel;
        if (AppShell.Maps.TryGetValue(vm.GetType(), out IEventMap[] maps))
        {
            foreach (var map in maps)
            {
                map.Wire(vm);
            }
        }
    }

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

        var vm = this.BindingContext as BaseViewModel;
        if (AppShell.Maps.TryGetValue(vm.GetType(), out IEventMap[] maps))
        {
            foreach (var map in maps)
            {
                map.Unwire(vm);
            }
        }
    }
}

Adding a Mapping

So far I’ve shown you what an existing mapping looks like and how it connects viewmodel events with an action, typically a navigation of some description. Let me walk you through adding another, so you can see how it works. We’ll add the ability to delete an item to the ItemDetailViewModel. We’ll add a command, DeleteItemCommand, which will be data bound to a button on the ItemDetailsPage. When invoked the DeleteItemCommand will issue a message, specifying the item to delete. After sending a message, the ItemDetailViewModel raises the ItemDeleted event.

public class ItemDetailViewModel : BaseViewModel
{
    public event EventHandler<Item> ItemDeleted;
    public ICommand DeleteItemCommand { get; }

    public Item Item { get; set; }
    public ItemDetailViewModel(Item item = null)
    {
        Title = item?.Text;
        Item = item;

        DeleteItemCommand = new Command(() =>
        {
            MessagingCenter.Send(this, "DeleteItem", Item);
            ItemDeleted?.Invoke(this, Item);
        });
    }
}

When the ItemDeleted event is raised, that’s the indication that this viewmodel is done and that the application should return to the previous page. For this, we just need to add a mapping to the AppShell class.

Maps.For<ItemDetailViewModel>()
    .Do(new EventHandler<Item>((s, args) => Navigation.PopAsync()))
    .When((vm, a) => vm.ItemDeleted += a, (vm, a) => vm.ItemDeleted -= a);

And that’s all there is to it – we can simply add events to the viewmodels; and maps to the AppShell.

Inner Working

Now that I’ve shown you how it works, let’s drill into what makes it work, starting with the EventMap class, which will be used to map an action to a viewmodel event.

public interface IEventMap
{
    void Wire(object viewModel);
    void Unwire(object viewModel);
}

public class EventMap<TViewModel, TDelegate>: IEventMap
{
    public TDelegate Action { get; set; }
    public Action<TViewModel, TDelegate> Arrive { get; set; }
    public Action<TViewModel, TDelegate> Leave { get; set; }

    public EventMap(
            TDelegate action, 
            Action<TViewModel, TDelegate> arrive, 
            Action<TViewModel, TDelegate> leave)
    {
        Action = action;
        Arrive = arrive;
        Leave = leave;
    }

    public void Wire(object viewModel)
    {
        Arrive((TViewModel)viewModel, Action);
    }

    public void Unwire(object viewModel)
    {
        Leave((TViewModel)viewModel, Action);
    }
}

The EventMap class is relatively simple and you can see that the Wire and Unwire methods invoke the corresponding Arrive and Leave methods. And yes, there is some assumptions made here without error handling around the casting of the videwModel object to the appropriate type.

The last bit of magic is the way that the mappings are created using a fluent style of coding. This is actually just a bunch of extension methods with a couple of builder classes added for good measure

public static class EventMapHelpers
{
    public static EventMapBuilderViewModel<TViewModel> For<TViewModel>(this IDictionary<Type, IEventMap[]> maps)
    {
        return new EventMapBuilderViewModel<TViewModel>() { Maps = maps };
    }

    public static EventMapBuilderViewModelEvent<TViewModel, TDelegate>
        Do<TViewModel, TDelegate>(this EventMapBuilderViewModel<TViewModel> viewModelBuilder, TDelegate viewModelEvent)
    {
        return viewModelBuilder.BuildWithEvent(viewModelEvent);
    }
    public static void When<TViewModel, TDelegate>(this EventMapBuilderViewModelEvent<TViewModel, TDelegate> builder, Action<TViewModel, TDelegate> arrive, Action<TViewModel, TDelegate> leave)
    {
        var key = typeof(TViewModel);
        var map = new EventMap<TViewModel, TDelegate>(builder.ViewModelAction, arrive, leave);
        if (builder.Maps.TryGetValue(key, out IEventMap[] existing))
        {
            builder.Maps[key] = existing.Union(new[] { map }).ToArray();
            return;
        }
        builder.Maps[key] = new[] { map };
    }
    public class EventMapBuilderViewModelEvent<TViewModel, TDelegate> :
        EventMapBuilderViewModel<TViewModel>
    {
        public TDelegate ViewModelAction { get; set; }
    }
    public class EventMapBuilderViewModel<TViewModel>
    {
        public IDictionary<Type, IEventMap[]> Maps { get; set; }

        public EventMapBuilderViewModelEvent<TViewModel, TDelegate> BuildWithEvent<TDelegate>(TDelegate viewModelAction)
        {
            return new EventMapBuilderViewModelEvent<TViewModel, TDelegate> { ViewModelAction = viewModelAction, Maps = Maps };
        }
    }
}

MVVM Navigation Summary

Whilst only a thought experiment, I think I’ve shown in this article the importance of having a structured approach to MVVM. Whether you choose to go with Prism or MvvmCross, or perhaps even choose your own pattern, the reality is that you need some structure to your code.

For anyone interested, you can grab my source code from here

2 thoughts on “MVVM Navigation with Xamarin.Forms Shell”

    • Yep but that wasn’t the point of the exercise 🙂
      To fix it you just need to change the Subscribe method in the ItemsViewModel to use NewItemViewModel instead of NewItemPage as the first type argument.
      I also didn’t tidy up the code, or implement a listener for the DeleteItem message that the Delete button sends.

      Reply

Leave a comment