Mastodon

Monday, September 8, 2014

Navigation With MVVM

In my previous post I covered the basics of navigation patterns in mobile apps and the Xamarin.Forms navigation APIs. In this post I will cover architectural issues related to navigation in an app using the MVVM pattern and one method of implementing your navigation in a more portable way.

The Problem

MVVM is a powerful design pattern for implementing cross-platform applications with UIs. Xamarin.Forms is based on MVVM, and it owes much of its portability to that pattern. The intent of MVVM is to try to separate platform-specific view code from potentially reusable logic (which may be model or view related). Typically you want to put as much logic into your View Model and Model as you can (thus increasing your reusable/portable code) and keep your View very dumb. The View should just be about getting information on the screen and translating user input into events that can be handled by the View Model.

If you implement the MVVM pattern correctly then you should be able to put your Model, View Model, and View each into their own assemblies. The View Model assembly should not reference the View assembly, and the Model assembly should not reference either the View or View Model assemblies. In a Xamarin.Forms application only the View assembly should include a reference to Xamarin.Forms1. That way both the Model and View Model could be reused with no code modification while swapping out the View assembly (maybe to one that uses Windows Phone APIs directly).

Navigation can be a bit of a challenge in MVVM because in theory the logic of which screen should be on the screen when is the domain of the View Model, but the APIs for actually moving from one screen to another are platform-specific2. This leads many people to putting their navigation logic in the View and then duplicating that logic for each platform.

In order to keep the navigation logic in the View Model where it belongs we need to solve a few problems:

  1. Create a new View given a new View Model (representing the new page/screen).
  2. Trigger the platform-specific navigation to that new View.

In order to accomplish this in the cross-platform View Model component we need some kind of interface and some dependency injection to get the platform-specific implementation.

My Solution

I have created an example implementation of this approach. You can view the source code and an example application on GitHub. Let's walk through how it works.

The core of this implementation is the IViewModelNavigation interface:

public interface IViewModelNavigation
{
    Task<object> PopAsync();

    Task<object> PopModalAsync();

    Task PopToRootAsync();

    Task PushAsync(object viewModel);

    Task PushModalAsync(object viewModel);
}

If you read my last post or have used the Xamarin.Forms navigation then this interface will look familiar. That's because I just copied the INavigation interface from Xamarin.Forms and then changed all of the Page arguments into View Models (as type object). As a result there is no longer anything in this interface that depends on Xamarin.Forms. It could be used on any platform. This allows you to create your View Model with navigation logic and then swap out all of your view code. Xamarin.Forms is portable, but maybe you prefer to have your Windows Phone apps use the more powerful native APIs.

We'll get to usage of that interface shortly, but first let's look at how you get an instance of it. In my example application take a look at App.cs:

public static Page GetMainPage()
{
    var mainPageViewModel = new MainPageViewModel();
    // ...
    var navigationFrame = new NavigationFrame(mainPageViewModel);
    return navigationFrame.Root;
}

This code is part of the View (the App class is platform-specific2). This is where the dependency injection comes from. Here we create an instance of the NavigationFrame class, which is the object that implements IViewModelNavigation for Xamarin.Forms applications. The constructor for NavigationFrame takes an initial view model (for the first screen). Once constructed we access the Root property, which is a NavigationPage (the basis for navigation in Xamarin.Forms) and return that as our app's main page.

The next thing to note here is that when we create the NavigationFrame we give it a View Model, not a Page. Xamarin.Form's NavigationPage class takes a Page (a View) for its first screen, but this API takes a View Model instead. It then creates the View (a Page) for you and uses that when constructing the NavigationPage.

Now that we have our first View on the screen how do we navigate to another view? Take a look at the HandleItemSelected method in the MainPageViewModel class:

private void HandleItemSelected(object parameter)
{
    ViewModelNavigation.PushAsync(new ItemPageViewModel() { Item = parameter as RssItem });
}

ViewModelNavigation is our IViewModelNavigation interface (more on that property soon), and you can see that we are calling one of the methods from that interface. Again, when we call this method we give it a View Model, not a View. The View Model is portable, whereas the View is not. We want this logic to stay in the View Model, but we want it to remain portable by keeping any View types out of it.

Where did this ViewModelNavigation property come from? If you look at the PageViewModel base class you will see that we implement the INavigatingViewModel interface. Here is the definition of that interface:

public interface INavigatingViewModel
{
    IViewModelNavigation ViewModelNavigation { get; set; }
}

All it does is provide a property that allows the Navigation API to inject itself into any View Models that it is given. This is optional (you can use your own mechanism to pass around the IViewModelNavigation instance), but it makes things a lot easier. Again, this mirrors the Navigation property in the VisualElement class of Xamarin.Forms. The design of this API lets you write your navigation code in nearly the same way that you already did in Xamarin.Forms, but you can move that code from your View into your View Model.

So far we've seen how the second requirement is solved (using dependency injection), but what about the first? Somehow we need to be able to take a View Model and create a View for it. That requires a factory of some sort.

My implementation uses reflection to handle this. First, you need to set up the association between your View and your View Model. To do this you just use the RegisterViewModelAttribute to decorate your View like this:

[RegisterViewModel(typeof(MainPageViewModel))]
public partial class MainPage : ContentPage
{
    // ...
}

When the Navigation API initializes it searches through the loaded assemblies for these attributes and sets up a dictionary mapping ViewModel types to View types. Then, whenever it needs to create a View from a View Model it looks up the right type from the dictionary, constructs the new View, and assigns the View Model as the BindingContext of the newly created View. It also assigns itself to the ViewModelNavigation property of the View Model (if it implements INavigatingViewModel). Then all it has to do is call the Xamarin.Forms navigation API to navigate to the new View.

That's basically all there is to it. With this approach you can now keep your navigation logic where it belongs and improve the portability of your code.

Downsides of My Solution

One potential downside of my current approach is its use of reflection. Reflection is a powerful way of marking up code in a clean, declarative way that keeps things decoupled, but it also comes with a runtime cost. At startup we have to go through all the types that are loaded looking for those attributes. That takes time, and that time grows as your application grows. Eventually this may lead to an unacceptable startup time cost. As an alternative you could initialize this mapping more directly in your code.

It also makes a classic tradeoff between portability and flexibility. The navigation API provided by this implementation is a bare-bones, lowest-common-denominator API that is supportable in some form by any mobile platform. There are many things you can do in platform-specific APIs that you can't do with this API. You could try to mitigate that by making a more extensible API, perhaps with a flexible mechanism of providing hints to the platform-specific implementation about how it might handle the navigation on each platform. The basic idea of using dependency injection and a View factory is still useful, but there may be some middle ground when it comes to how much information about the platforms should leak into the API.

On a similar note, another thing to consider is that this API still assumes a specific style of navigation that is most applicable to mobile applications. View Models can be very portable, but they still represent specific views. An application that is portable between desktop and mobile platforms may share all of the Model layer, but their UX is almost certainly very different. It doesn't make much sense to try to share the View Model layer between radically different UX implementations. The navigation logic should likewise be portable between mobile platforms or between desktop platforms, but probably not between both. My suggestion is to not try to share your View Model component between desktop and mobile platforms, and this is just yet another reason for that.

Other Approaches and Extensions

There are many other potential approaches to this kind of problem. Most are going to involve somehow communicating from the View Model layer to the View layer. In my case I used dependency injection directly, but another approach, outlined by Alec Tucker on his blog, is to use the Xamarin.Forms MessagingCenter API to request the View to perform a navigation. There are pros and cons to each approach so I'll let the reader make up his own mind about which he prefers.

This approach could also be extended to support more kinds of navigation and other platforms. I provided a Xamarin.Forms implementation as a reference, but it should be possible to create other implementations for other platforms.


  1. In practice this may be harder than I would like it to be. There are some useful types in Xamarin.Forms, like Command, that would require more work to decouple. Still, it is possible using more dependency injection. 

  2. Note that I am considering Xamarin.Forms to be a platform. When implementing a View you could many possible APIs, including raw Xamarin.iOS, Xamarin.Android, or Windows Phone APIs. Xamarin.Forms also lets you write view code using a specific API, but that API introduces a dependency that you may want to change in the future. If you use MVVM properly you should be able to reuse 100% of your Model and View Model even while switch between Xamarin.Forms and some other API. 

3 comments:

  1. Hi Adam, firstly thanks for this post, I like the solution you have put forward for navigating with MVVM design pattern. I have a question regarding data binding of properties that is related to this implementation, and keeping the view as "dumb" as possible.

    I have a property in my view model (WidgetData) which is of type Queue<WidgetDataModel>. The viewmodel exposes this as a bindable property by conforming to the INotifyPropertyChanged interface and raising the PropertyChanged event when the property is updated.

    I have a number of views that use this property, one being a ListView. In the case of the ListView I can simply call SetBinding(ListView.ItemSourceProperty, "WidgetData") to bind the lists ItemSourceProperty to the WidgetData. This works fine.

    However, I have another view that needs to process the data a bit more, which means I cant simply bind it to a predefined BindableProperty, so I am trying to create my own. This is where I hit the issue. If I create a BindableProperty, it seems that I need to pass it a returnType on construction. The view has no knowledge of the WidgetDataModel class, so I cannot use that type. I have tried to pass it Queue<object> type, or just object type, but this seems to cause the binding to fail.

    The workaround I have come up with is to make the WidgetData property of type Queue<IDictionary<string,object>>, then reference the elements I need in the binding by using [key]. This works but I am wondering how the ListView.ItemSourceProperty is constructed, as this must be constructed in some sort of generic way for the binding to work correctly.

    Apologies if this is not the right place to post - please remove if not.

    Many thanks, Dan.

    ReplyDelete
    Replies
    1. First, a clarification. A "bindable property" is a property that can be a target for binding. For instance, ListView.ItemsSourceProperty is a BindableProperty because you can call SetBinding using it as the property to bind. The source (the property in your view model) does not need to be bindable. Simply implementing INotifyPropertyChanged does not make a property "bindable", it just makes it suitable as a source for a binding. So you shouldn't call that a "bindable property".

      As to your question, I think I need to question one of your premises: "The view has no knowledge of the WidgetDataModel class, so I cannot use that type." Why not? It's appropriate, and typical, for a view to have knowledge of the view model. It's certainly not required since binding is done by name, and reflection is used to find the CLR properties, but there's no reason you can't make your view explicitly aware of the view model type. The view model should never be aware of the view, but the view can (and very often does) know about the view model.

      So my recommendation would be to make your new bindable property in your view use the type that's appropriate.

      The ItemsSource property in ListView (actually in ItemsView) simply uses the non-generic IEnumerable type (IEnumerable extends IEnumerable). If there were a situation where you needed a view to be entirely agnostic about the type of its BindingContext then that is basically how you do it. Just don't use a generic collection type that forces you to specify the type of element.

      Delete
    2. Thanks for the clarification, I got my terminology mixed up.

      Also, thanks for answering my question, this clears things up quite a lot. On reflection, I don't have a good reason to keep knowledge of the view model, or model in my case, from the view. I am suffering from a bit of information overload as I have only been learning C# for a couple of weeks, so trying to get my head around a new language + the MVVM pattern is a fairly steep learning curve, but it is becoming a lot clearer to me, with help from people like yourself.

      Thanks again.

      Delete