How to Navigate Through a Xamarin.Forms App

Navigation in the MVVM architectural pattern seems a pretty tough task and most commonly found solutions don’t really satisfy me.
They are overly complex, involve lots of code or don’t seem elegant at all. Some solutions just statically specify the target page as a string (in Windows/Windows Phone/UWP apps):

this.NavigationService.Navigate("MainPage");

In Xamarin.Forms, navigation requires you to specify an instance of Page to navigate to. Usually, you would write code like this:

this.Navigation.Navigate(new SubPage());

This is bad for a couple of reasons:

  1. Navigation is a property of the Page type – you’ll have to somehow pass this to your ViewModel or have some kind of glue in your code behind.
  2. Since we’re calling the constructor of SubPage here, we’re coupling these classes tightly. This means we cannot comfortable test the navigation portion in our ViewModel.
  3. Navigation without Pages is not possible – that means that unit tests would rely on the Xamarin.Forms framework, which they should not need to.

In this article, I will present a pattern that – at least in my eyes – solves these problems while still allowing easy configurability and short, elegant code. Let’s get started.

tl;dr: Loading off the navigation to the App class saves us a lot of headache. You can check out the Github Repository for a full, working example.

Ground Work: Interfaces

For this pattern, we will require three interfaces: IViewModel, INavigationService and IAppNavigationResolver.

IViewModel

public interface IViewModel {
    Task ActivateAsync();
}

This interface is very straightforward: Every ViewModel has an async method called ActivateAsync, which will be called whenever the ViewModel is activated (i.e. the page it is used with is brought to the front).

INavigationService

public interface INavigationService {
    INavigationService RegisterPage<TViewModel, TPage>()
        where TViewModel : IViewModel
        where TPage : new();

    Task<TViewModel> NavigateAsync<TViewModel>()
        where TViewModel : IViewModel;

    Task<IViewModel> GoBackAsync();
}

This defines the interface of our actual NavigationService (which will be injecting using Dependency Injection later). It mainly has two interesting methods: NavigateAsync and GoBackAsync.
These methods are used to perform navigation in the app: NavigateAsync navigates to the page that is associated with the specified ViewModel, while GoBackAsync will return to the most recent page.
Both methods return the page’s ViewModel such that additional actions may be performed (such as passing a parameter).

Since GoBackAsync does not have any details about the page it will navigate to, it returns the ViewModel as a IViewModel, while NavigateAsync uses a concrete type (or an interface).

The RegisterPage method is not strictly needed in the interface, as it will only be called during configuration. However, I like to keep the method in the interface to make it accessible just in case.

IAppNavigationResolver

public interface IAppNavigationResolver {
    Task NavigateAsync<TViewModel>(Type pageType, TViewModel viewModel)
        where TViewModel : IViewModel;
    Task<IViewModel> GoBackAsync();
}

The implementation of this interface does all the heavy lifting: It provides the actual navigation facilities and creates the instances of Page to navigate to.
The interface will later be implemented by our App class.
Both methods in this interface are congruent to the ones in INavigationService, except for the fact that NavigateAsync expects a Type (the concrete type of the Page) and an IViewModel rather than just type parameters.

Step 2: Implement the Interfaces

I will only show the implementation of INavigationService and IAppNavigationResolver here, as IViewModel only serves to define the ActivateAsync method.
I will also use an interface called IDependencyContainer, which really is just an abstraction over Dependency Injection contains such as the one provided by Unity.

NavigationService

public class NavigationService : INavigationService {
    private readonly IAppNavigationResolver appNavigationResolver;
    private readonly IDependencyContainer dependencyContainer;
    private readonly Dictionary<Type, Type> registeredPages = new Dictionary<Type, Type>();

    public NavigationService(
        IAppNavigationResolver appNavigationResolver,
        IDependencyContainer dependencyContainer
    ) {
        this.appNavigationResolver = appNavigationResolver;
        this.dependencyContainer = dependencyContainer;
    }

The NavigationService requires two services: an IAppNavigationResolver (our actual workhorse) and an IDependencyContainer, which will be used to instantiate our ViewModels.

    public async Task<IViewModel> GoBackAsync() {
        var viewModel = await this.appNavigationResolver.GoBackAsync();
        await viewModel.ActivateAsync();

        return viewModel;
    }

This method is relatively straightforward: Get the new ViewModel by causing the resolver to go back, then re-activate the ViewModel asynchronously.

    public async Task<TViewModel> NavigateAsync<TViewModel>()
        where TViewModel : IViewModel {
        Type pageType;
        if(!registeredPages.TryGetValue(typeof(TViewModel), out pageType)) {
            throw new InvalidOperationException($"No View registered for ViewModel type {typeof(TViewModel).Name}");
        }

        var viewModel = this.dependencyContainer.Resolve<TViewModel>();

        await viewModel.ActivateAsync();

        await this.appNavigationResolver.NavigateAsync(pageType, viewModel);
        return viewModel;
    }

Not much logic here, either. First, the Page type for the requested ViewModel type is looked up in the (ViewModel type, Page type) dictionary (registeredPages).
If no type can be found, an InvalidOperationException is thrown. Next, the ViewModel is instantiated using the Dependency Injection container and then activated.

Finally, this method asks the navigation resolver to navigate to the specified page and provide the page with the freshly instantiated ViewModel.

    public INavigationService RegisterPage<TViewModel, TPage>()
        where TViewModel : IViewModel
        where TPage : new() {
        this.registeredPages.Add(typeof(TViewModel), typeof(TPage));

        return this;
    }
}

This method is used to register a (ViewModel type, Page type) association. The associations are saved in the registeredPages Dictionary.
The method then returns the instance itself in order to allow chaining calls.

The Workhorse: Let App implement IAppNavigationResolver

When you open your App.xaml.cs, you will find the App class. We will have to make this class implement IAppNavigationResolver.

First, change the class header to implement the interface:

public partial class App : Application, IAppNavigationResolver {

Next, implement the methods defined by IAppNavigationResolver:

public async Task NavigateAsync<TViewModel>(Type pageType, TViewModel viewModel) where TViewModel : IViewModel {
    var page = (Page)Activator.CreateInstance(pageType);
    page.BindingContext = viewModel;

    await ((NavigationPage)this.MainPage).Navigation.PushAsync(page);
}

This is using the Activator class to instantiate an object of the Type specified by pageType, which is then cast to Page.
This allows us to access its BindingContext property and set it to the ViewModel that was passed to the method.

Next, the actual navigation is performed.
This is easy since the App class (or rather its base class Application) has a property called MainPage, which is set at application start-up and can be used to get the main page at any time.

public async Task<IViewModel> GoBackAsync() {
    await ((NavigationPage)this.MainPage).Navigation.PopAsync();

    var newPage = ((NavigationPage)this.MainPage).CurrentPage;
    return (IViewModel)newPage.BindingContext;
}

Navigating back is easy as well: First, pop the current page off the navigation stack. Then get the current page (which can be accessed via the main page).
Cast the current page’s BindingContext to IViewModel and return it – that’s all.

Wiring It All up

Let’s now wrap up this project by wiring up the components we have written so far: We will need to register our types with the Dependency Injection container of our choice, register the ViewModel and Page types with our navigation service and define our main page.
All of this will happen in our App class’s constructor:

    public App() {
        InitializeComponent();

        var container = new UnityDependencyContainer()
            .Register<ISubViewModel, SubViewModel>()
            .Register<IMainViewModel, MainViewModel>();

        var navigationService = new NavigationService(this, container);
        container.Register<INavigationService>(navigationService);

        navigationService
            .RegisterPage<IMainViewModel, MainPage>()
            .RegisterPage<ISubViewModel, SubPage>();

        var viewModel = container.Resolve<IMainViewModel>();
        viewModel.ActivateAsync();

        MainPage = new NavigationPage(new MainPage() {
            BindingContext = viewModel
        });
    }

The first few lines instantiate an UnityContainer (abstracted away using the IDependencyContainer interface I mentioned earlier) and registers the services and ViewModels.

Next, we instantiate our NavigationService, which requires a reference to the App instance as well as the Dependency Injection container.
Of course, the navigation service needs to be registered with the container as well in order to allow ViewModels to require it.

Next, we register the ViewModel and Page types with the navigation service. In this case, calling Navigate<IMainViewModel>() on the navigation service will cause the app to navigate to the MainPage,
while calling Navigate<ISubViewModel>() will navigate to the SubPage.

Unfortunately, we cannot (yet?) navigate to the very first page in the app using the navigation service, so we have to use the container directly to resolve the ViewModel of the first page (MainPage in this case).
This viewModel is then activated asynchronously (Note that this call is not awaited, since it happens in the constructor!) and passed to a new instance of MainPage as the BindingContext.

Final Step: Use It!

We have now set up our application for navigation and can test it out. For this, We can define two very simple ViewModels:

public interface IMainViewModel : IViewModel {
    ICommand SubCommand { get; }
}
public interface ISubViewModel : IViewModel {
    ICommand GoBackCommand { get; }
    String Something { get; }
    void Configure(string something);
}

These interfaces define what our application will be able to to: Both define a single ICommand to navigate back and forth between the pages. Now let’s implement these interfaces:

public class MainViewModel : IMainViewModel {
    private INavigationService navigationService;

    public ICommand SubCommand { get; }

    public MainViewModel(INavigationService navigationService) {
        this.navigationService = navigationService;
        this.SubCommand = new RelayCommand(Navigate);
    }

    private async void Navigate() {
        var viewModel = await this.navigationService.NavigateAsync<ISubViewModel>();
        viewModel.configure("This is awesome!");
    }
}

The command in this ViewModel will cause the application to navigate to the second page, configuring it to display the string “This is awesome!”.
The second ViewModel will then make this text available as a property (and also implements INotifyPropertyChanged to notify the View of the change):

public class SubViewModel : ISubViewModel, INotifyPropertyChanged {
    public event PropertyChangedEventHandler PropertyChanged;
    private INavigationService navigationService;

    public ICommand GoBackCommand { get; }

    private string something;
    public string Something {
        get { return this.something; }
        set {
            this.something = value;
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Something)));
        }
     }

    public SubViewModel(INavigationService navigationService) {
        this.navigationService = navigationService;
        this.GoBackCommand = new RelayCommand(NavigateBack);
    }

    public void Configure(string something) {
        this.Something = something;
    }

    private async void NavigateBack() {
        await this.navigationService.GoBackAsync();
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *