Implementing ViewModel-based Navigation in your Universal App

Since the introduction XAML applications (WFP, Silverlight) and continuing with Windows 8 Metro-style Apps, a Universal Windows Platform App (UWP) takes advantage of the Model-View-ViewModel (MVVM) pattern to distribute the responsibilities of an app to different classes or layers.

In this post, I will walk you through implementing view model-based navigation in a UWP App, instead of the more intuitive Page-based navigation.
In order to make the explanation more complete, I will not make use of any framework for this post. In a later post, I will refactor the code in order to take advantage of one of many frameworks that include MVVM helpers.

For instance, starting from the Hamburger Menu application we’ve built in this previous post, we can introduce view model classes to refactor the navigation between pages and synchronize which option is selected when landing on a particular page. But first, let’s start with an initial situation:

  • First, follow the steps in the previous post to build a Hamburger Menu application.
  • Create a couple of additional pages, MainPage.xaml and OtherPage.xaml.

Remember that the Hamburger Menu is implemented using RadioButton controls.
In the Shell.cs file, add the following code to navigate to the pages when a RadioButton is selected:

public Frame AppFrame { get { return Content; } }

private void Option1Button_Checked(object sender, RoutedEventArgs e)
{
    AppFrame.NavigateTo(typeof (MainPage));
}

private void Option2Button_Checked(object sender, RoutedEventArgs e)
{
    AppFrame.NavigateTo(typeof (OtherPage));
}

...

Notice that the application currently uses the traditional Page-based navigation technique, reacting to event handler in the code-behind.

Introducing view model in the UWP App

One key benefit of view models is that they can help remove most of the code-behind a page. Using two-way databinding instead of wiring events in the code-behind usually lends to a better design.

Create the MainPageViewModel.cs and OtherPageViewModel.cs classes, that correspond to their respective pages. Create also the ApplicationViewModel.cs class that represents the view model for the entire application. A view model class looks like this:

public class MainPageViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private readonly INavigationService navigationService_;

    /// <summary>
    /// Initialize a new instance of the <see cref="MainPageViewModel"/> class with required dependencies.
    /// </summary>
    /// <param name="navigationService"></param>
    public MainPageViewModel(INavigationService navigationService)
    {
    	navigationService_ = navigationService;
    }

    ...

    private void RaisePropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
...

In a real-world program, a view model inherits from a common base class that provides helpers for property notifications. Please, note that, for this post, I’m not using any framework so that everything is made clear at the cost of a slightly longer post :-).

Notice that the MainPageViewModel class requires a dependency to the INavigationService interface. Here is what this is all about:

Creating a simple NavigationService

A good practice to follow when building a UWP application is to abstract away most features behind an interface to some kind of “service”.
So, we will create a simple INavigationService and its corresponding NavigationService implementation.

Because we want to abstract as many infrastructure details as possible, we will use simple string identifiers to represent the pages of our application.
The INavigationService interface can be as simple as :

/// <summary>
/// Provides a mechanism to navigate between pages.
/// </summary>
public interface INavigationService
{
    /// <summary>
    /// Gets the name of the currently displayed page.
    /// </summary>
    string CurrentPage { get; }

    /// <summary>
    /// Navigates to the specified page.
    /// </summary>
    /// <param name="page"></param>
    void NavigateTo(string page);

    /// <summary>
    /// Navigates to the specified page and
    /// supply additional page-specific parameters.
    /// </summary>
    /// <param name="page"></param>
    /// <param name="parameter"></param>
    void NavigateTo(string page, object parameter);

    /// <summary>
    /// Navigates to the previous page in the navigation history.
    /// </summary>
    void GoBack();
}

The implementation is straightforward:

/// <summary>
/// Provides a mechanism to navigate between pages.
/// </summary>
public sealed class NavigationService : INavigationService
{
    private readonly IDictionary<string, Type> pages_
        = new Dictionary<string, Type>();

    /// <summary>
    /// The name of the virtual "root" page at the top of the navigation history.
    /// </summary>
    public const string RootPage = "(Root)";

    /// <summary>
    /// A moniker for an "unknown" page when navigation happens without
    /// using the <see cref="NavigationService"/>.
    /// </summary>
    public const string UnknownPage = "(Unknown)";

    private static Frame AppFrame => ((Window.Current.Content as Frame)?.Content as Shell)?.AppFrame;

    public void Configure(string page, Type type)
    {
        lock (pages_)
        {
            if (pages_.ContainsKey(page))
                throw new ArgumentException("The specified page is already registered.");

            if (pages_.Values.Any(v => v == type))
                throw new ArgumentException("The specified view has already been registered under another name.");

            pages_.Add(page, type);
        }
    }

    #region INavigationService Implementation

    /// <summary>
    /// Gets the name of the currently displayed page.
    /// </summary>
    public string CurrentPage
    {
        get
        {
            var frame = AppFrame;
            if (frame.BackStackDepth == 0)
                return RootPage;

            if (frame.Content == null)
                return UnknownPage;

            var type = frame.Content.GetType();

            lock (pages_)
            {
                if (pages_.Values.All(v => v != type))
                    return UnknownPage;

                var item = pages_.Single(i => i.Value == type);

                return item.Key;
            }
        }
    }

    /// <summary>
    /// Navigates to the specified page.
    /// </summary>
    /// <param name="page"></param>
    public void NavigateTo(string page)
    {
        NavigateTo(page, null);
    }

    /// <summary>
    /// Navigates to the specified page and
    /// supply additional page-specific parameters.
    /// </summary>
    /// <param name="page"></param>
    /// <param name="parameter"></param>
    public void NavigateTo(string page, object parameter)
    {
        lock (pages_)
        {
            if (!pages_.ContainsKey(page))
                throw new ArgumentException("Unable to find a page registered with the specified name.");

            System.Diagnostics.Debug.Assert(AppFrame != null);
            AppFrame.Navigate(pages_[page], parameter);
        }
    }

    /// <summary>
    /// Navigates to the previous page in the navigation history.
    /// </summary>
    public void GoBack()
    {
        System.Diagnostics.Debug.Assert(AppFrame != null);
        if (AppFrame.CanGoBack)
            AppFrame.GoBack();
    }

    #endregion
}

The NavigationService class includes the Configure method that is used to associate a simple string identifier to a data type. This method will be called when registering dependencies and implementing the view modelLocator further in this post.

You’ll notice that this implementation is using the Page-based navigation that is dependent from the infrastructure on which this application runs. Putting this service behind the INavigationService interface allows to decouple the implementation from the application code. This will make it possible to provide alternate implementations for other platforms, such as iOS or Android using Xamarin, for instance.

Users of MVVMLight will recognize part of the implementation of its Navigation Service. One key difference, however, is the use of the following line:

private static Frame AppFrame => ((Window.Current.Content as Frame)?.Content as Shell)?.AppFrame;

This represents the top-level frame that is hosted in the Shell’s SplitView pane. This allows to keep the Hamburger Menu on all pages and still navigate between different views.

As per this Stack Overflow Question it seems that the default MVVMLigth implementation of the Navigation Service does not work well with Hamburger Menu-based applications such as the one I’m showing here. Fortunately, as we’ve seen, implementing a Navigation Service from scratch is straightforward.

More Scaffolding Required

Because we’re using services and would like to code to their interfaces, we will really benefit from using some form of dependency injection. Here, any IoC container or framework will do but, again, for the purpose of this post, let’s use a simple implementation like this one:

/// <summary>
///  A singleton dependency container.
/// </summary>
public sealed class DependencyContainer : IDependencyContainer, IDependencyResolver
{
    private readonly IDictionary<Type, Type> registrations_
        = new Dictionary<Type, Type>();

    private readonly IDictionary<Type, Object> instances_
        = new Dictionary<Type, Object>();

    private static readonly object[] NoArguments = new object[0];

    private static readonly object SyncLock = new object();

    private DependencyContainer()
    {
    }

    /// <summary>
    /// Gets the default instance of the <see cref="DependencyContainer"/> class.
    /// </summary>
    public static DependencyContainer Default { get; } = new DependencyContainer();

    /// <summary>
    /// Registers a new mapping between the specified types.
    /// </summary>
    /// <param name="type"></param>
    /// <param name="mappedTo"></param>
    public void RegisterType(Type type, Type mappedTo)
    {
        if (instances_.ContainsKey(type))
            throw new Exception("A registration for this type already exists.");
        if (registrations_.ContainsKey(type))
            throw new ArgumentException("The specified interface type has already been registered.");

        registrations_.Add(type, mappedTo);
    }

    /// <summary>
    /// Registers a new mapping between the specified type and the specified instance.
    /// </summary>
    /// <param name="type"></param>
    /// <param name="instance"></param>
    public void RegisterInstance(Type type, object instance)
    {
        if (instances_.ContainsKey(type))
            throw new Exception("A registration for this type already exists.");
        if (registrations_.ContainsKey(type))
            throw new ArgumentException("The specified interface type has already been registered.");

        instances_.Add(type, instance);
    }

    /// <summary>
    /// Returns a registered instance corresponding to the specified type.
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public object Resolve(Type type)
    {
        lock (SyncLock)
        {
            if (!instances_.ContainsKey(type))
            {
                if (!registrations_.ContainsKey(type))
                    throw new Exception($"Unable to resolve an instance of the specified type '{type.FullName}'. Are you missing a type registration?");

                var mappedTo = registrations_[type] ?? type;
                var constructors = mappedTo.GetConstructors();
                if (constructors.Length > 1)
                    throw new Exception($"Unable to resolve an type that has more than one constructor.");

                var constructor = constructors[0];
                var parameters = constructor.GetParameters();
                if (parameters.Length == 0)
                    instances_[type] = constructor.Invoke(NoArguments);
                else
                {
                    var args = new object[parameters.Length];
                    foreach (var parameter in parameters)
                        args[parameter.Position] = Resolve(parameter.ParameterType);

                    instances_[type] = constructor.Invoke(args);
                }
            }

            return instances_[type];
        }
    }
}

This code is inspired freely from a sample found here. The bulk of the code is in the Resolve method that uses reflection to dynamically invoke the constructor on types you want to resolve.

Basically, there are two kind of registrations :

  • Mapping between types (usually, from an instance type to an implementing type).
  • Mappings between types and concrete instances.

If an instance is already registered for a given type, it is returned directly.

If not, the method attempts to find the (instance) type mapped to the specified (interface) type. Using reflection, the method looks up a single constructor and identifies the list of its parameters. If no parameters are required, the method instantiates an object of this type and adds a mapping for subsequent use. If more parameters are required, the method is called recursively to resolve an instance for each required parameter, building a list that is supplied in a call to the constructor.

Please, note that this code does not support resolving types with more than one constructor. For a more complete implementation, refer to the aforementioned MVVMLight framework, or use an alternate implementation such as Unity, etc.

For syntactic sugar, I like to include additional methods via an extension class:

public static class DependencyContainerExtensions
{
    /// <summary>
    /// Registers a concrete instance of a particular type to the dependency container.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="container"></param>
    /// <param name="instance"></param>
    public static void RegisterInstance<T>(this IDependencyContainer container, object instance)
    {
        container.RegisterInstance(typeof (T), instance);
    }

    /// <summary>
    /// Registers a mapping from a specified type to itself to the dependency container.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="container"></param>
    public static void RegisterType<T>(this IDependencyContainer container)
    {
        container.RegisterType(typeof (T), typeof (T));
    }

    /// <summary>
    /// Registers a mapping between two specified types to the dependency container.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <typeparam name="TU"></typeparam>
    /// <param name="container"></param>
    public static void RegisterType<T, TU>(this IDependencyContainer container)
    {
        container.RegisterType(typeof(T), typeof(TU));
    }

    /// <summary>
    /// Resolves an instance mapped from the specified type.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="resolver"></param>
    /// <returns></returns>
    public static T Resolve<T>(this IDependencyResolver resolver)
    {
        return (T) resolver.Resolve(typeof (T));
    }
}

One final piece of code that will help us is to implement a view modelLocator object in our app. This class is responsible for returning instances of a given view model and usually takes advantage of the dependency container shown above. Here is a simple implementation of a view modelLocator for this particular application:

/// <summary>
/// Provides access to the view model classes used by this application.
/// </summary>
public sealed class view modelLocator
{
    /// <summary>
    /// Initialize a new instance of the <see cref="ViewModelLocator"/> class.
    /// </summary>
    public view modelLocator()
    {
        var container = DependencyContainer.Default;

        // dependencies

        container.RegisterType<ISettingsService, SettingsService>();

        // register view models

        container.RegisterType<ApplicationViewModel>();
        container.RegisterType<MainPageViewModel>();
        container.RegisterType<OtherPageViewModel>();

        // navigation service

        var navigationService = new NavigationService();
        navigationService.Configure("MainPage", typeof (MainPage));
        navigationService.Configure("OtherPage", typeof (OtherPage));

        container.RegisterInstance<INavigationService>(navigationService);
    }

    /// <summary>
    /// Gets the <see cref="ApplicationViewModel"/> associated with the <see cref="Shell"/> page.
    /// </summary>
    public ApplicationViewModel Application => DependencyContainer.Default.Resolve<ApplicationViewModel>();

    /// <summary>
    /// Gets the <see cref="MainPageViewModel"/> associated with the <see cref="MainPage"/> page.
    /// </summary>
    public MainPageViewModel MainPage => DependencyContainer.Default.Resolve<MainPageViewModel>();

    /// <summary>
    /// Gets the <see cref="OtherPageViewModel"/> associated with the <see cref="OtherPage"/> page.
    /// </summary>
    public OtherPageViewModel OtherPage => DependencyContainer.Default.Resolve<OtherPageViewModel>();
}

The primary purpose of this class is to provide access to all view model objects used in this application. Specifically, the three property getters are used via databinding to associated a particular view model objects to its respective page in the XAML DataContext.

For the purpose of this post, I have also centralized the registration of all required dependencies in the constructor of the ViewModelLocator class. When an instance of a particular view model object is resolved, the dependency container will resolve its INavigationService dependency.

Finally, this class also uses the NavigationService directly to configure a string identifier for each page of the application.

Putting it all together

First, we need to register a single instance of the ViewModelLocator in the application. This can be done in the application’s resource dictionary. In the App.xaml file, include the following code:

<Application.Resources>
  <ResourceDictionary>
    ...
    <vm:ViewModelLocator xmlns:vm="using:App.ViewModels" x:Key="ViewModelLocator" />
  </ResourceDictionary>
</Application.Resources>

In order to associate the corresponding view model object for each page in the DataContext, add a declaration in the XAML file. For instance, here is the association of the ApplicationViewModel view model in the Shell.xaml page:

<Page
    x:Class="App.Shell"
    ...
    DataContext="{Binding Path=Application, Source={StaticResource ViewModelLocator}}"
    >

 

Here Application refers to the name of the property in the ViewModelLocator that returns an instance of the appropriate ApplicationViewModel object.

In order to substitute event handlers in the code-behind with properties from the view model bound to some controls, we need to add the following properties to the ApplicationViewModel:

/// <summary>
/// Represents the ViewModel for the entire application.
/// </summary>
public sealed class ApplicationViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private readonly INavigationService navigationService_;

    /// <summary>
    /// Initialize a new instance of the <see cref="ApplicationViewModel"/> class.
    /// </summary>
    /// <param name="navigationService"></param>
    public ApplicationViewModel(INavigationService navigationService)
    {
        navigationService_ = navigationService;
    }

    /// <summary>
    /// Gets a value indicating whether the <see cref="MainPage"/> is currently displayed.
    /// </summary>
    public Boolean MainPageSelected
    {
        get { return navigationService_.CurrentPage == "MainPage"; }
        set
        {
            if (value)
                navigationService_.NavigateTo("MainPage");
        }
    }

    /// <summary>
    /// Gets a value indicating whether the <see cref="OtherPage"/> is currently displayed.
    /// </summary>
    public Boolean OtherPageSelected
    {
        get { return navigationService_.CurrentPage == "OtherPage"; }
        set
        {
            if (value)
                navigationService_.NavigateTo("OtherPage");                
        }
    }

    public void RaisePropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

The MainPageSelected and OtherPageSelected properties are defined as simple booleans to make it easy to databind them to the IsChecked state of the RadioButton controls used in the Hamburger Menu for the navigation.

The setters for these properties are used to trigger navigation when the corresponding RadioButton control is selected from the UI.

In the Shell.xaml file, remove the event handlers and use the following databindings instead:

This makes it easy to trigger navigation when a RadioButton is selected.
Conversely, when navigation changes the currently displayed page, the state for the corresponding RadioButton controls is updated.

Finally, in the Shell.cs file, remove the now unused event handlers.

Conclusion

I hope this post makes it easier for beginners to see what’s behind a simple navigation service in a Universal Windows Platform app. In the next posts, I will show how to refactor this code around one of many popular frameworks. I will also show how to record the currently displayed page on exit in order to navigate to the previously displayed page when the application starts.

Advertisements
This entry was posted in UWP and tagged , . Bookmark the permalink.

One Response to Implementing ViewModel-based Navigation in your Universal App

  1. Hi this is a great Tutorial, do you have the files for this and can you send it to me? Thanks!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s