Xamarin, Xamarin.Forms

A Responsive ViewModel – Part I

Last time, we talked about what’s missing in MVVM support with Xamarin.Forms, particularly in the realm of the ViewModel. We may have gotten lost in the weeds a bit, so let’s focus a bit more here. Our focal point will be creating a ViewModel that’s more responsive, including support for complex type parameters during navigation and responding to view appearing/disappearing events. We’ll use the previous article as a foundation for what we will build on today.

Recall the CreatePage method we created previously in ApplicationBase to contain the boiler plate that we need to tie together the bits contained in a Page that we would like to support in a ViewModel:

public Page CreatePage<TPage, TViewModel>(bool wrapInNavigationPage = false)
    where TPage : ContentPage
    where TViewModel : ViewModelBase
{
    var vm = default(TPage);
    var page = default(TViewModel);

    vm.SetWeakPage(page);
    page.BindingContext = vm;

    if (wrapInNavigationPage)
    {
        return new NavigationPage(page);
    }

    return page;
}

Here, we have already given ourselves the foundation and pieces necessary to support navigation parameters and allowing the ViewModel to respond to events that occur within the Page. This is because we have constrained both the creation of the Page and the ViewModel to our own method where we can define the lifecycle we desire.

Our first step is not to code, but to define how to represent our navigation parameters. Consider that the consumer would maybe like to send multiple parameters of the same type, e.g. int. There is no need to create our own distinct type for this. A Dictionary<string, object> would suffice as the consumer would use the string key to define unique parameters and the object value to contain the actual parameter they would like to pass.

Secondly, to support navigation parameters, we will create a more verbose API surface for CreatePage to allow different combinations of our navigation parameters and the boolean wrapInNavigationPage to be supplied. We do not want to force the consumer to use one, both or either parameter. Therefore, we’ll create an API surface that looks like the following.

Page CreatePage<TPage, TViewModel>(Dictionary<string, object> navigationParams = null)
Page CreatePage<TPage, TViewModel>(bool wrapInNavigationPage, Dictionary<string, object> navigationParams = null)

Now that we have an API that allows the consumer to provide navigation parameters, we need the ability to pass this information into the ViewModel. We do not want to inject these into the constructor of the ViewModel since it’s most likely/possible that the consumer’s implementation of ViewModelBase could do complex or long tasks with that information. A constructor should always be allowed to be constructed safely and as quickly as possible and we should assist the consumer in that approach. To that end, we can add a new method to our ViewModelBase that the consumer can choose to implement. Since we want it to be a choice for the consumer, we will favor a virtual method instead of an abstract one.

public virtual void Initialize(Dictionary<string, object> navigationsParams = null) { }

What’s left to wrap up the lifecycle of this simple implementation is to pass the navigation parameters we received in our CreatePage method to our Initialize method of our ViewModel. To do that, we only need to add the following line to our CreatePage method, which I’ll demonstrate more verbosely in a moment.

vm.Initialize(navigationParams);

Supporting Async/Await

Based on my experience thus far with Xamarin.Forms, it is a common demand and expectation that we would want to support async/await in this “initialize” flow. The result of passing navigation parameters can certainly lead to operations that would require it. To that end, we will update our virtual method Initialize to support async/await with a default implementation.

public virtual async Task Initialize(Dictionary<string, object> navigationsParams = null) 
            => await Task.CompletedTask;

This seemingly small change causes us to chain this async support up through our API and into our CreatePage method, where we will also need to support asynchronous operations. As such, our CreatePage implementation will now be implemented as follows.

public Task<Page> CreatePage<TPage, TViewModel>(Dictionary<string, object> navigationParams = null)
    where TPage : ContentPageBase
    where TViewModel : ViewModelBase
    => CreatePage<TPage, TViewModel>(false, navigationParams);

public async Task<Page> CreatePage<TPage, TViewModel>(bool wrapInNavigationPage, Dictionary<string, object> navigationParams = null)
    where TPage : ContentPageBase
    where TViewModel : ViewModelBase
{
    var vm = default(TViewModel); // future article
    var page = default(TPage); // future article

    vm.SetWeakPage(page);
    await vm.Initialize(navigationParams);

    page.BindingContext = vm;

    if (wrapInNavigationPage)
    {
        return new NavigationPage(page);
    }

    return page;
}

There’s one more piece of the puzzle for added simplicity for anyone consuming this. That piece is to add support to our PushAsync methods in ViewModelBase so the consumer can avoid invoking CreatePage directly and navigating to the newly created Page. While optional, this leads to cleaner implementations within derived classes of ViewModelBase. As we did with CreatePage, we’ll create additional overloads of PushAsync to support different combinations of parameters.

public Task PushAsync<TPage, TViewModel>(bool animated = true)
    where TPage : Page
    where TViewModel : ViewModelBase
    => PushAsyncInternal<TPage, TViewModel>(null, animated, false);

public Task PushAsync<TPage, TViewModel>(Dictionary<string, object> navigationParams, bool animated = true)
    where TPage : Page
    where TViewModel : ViewModelBase
    => PushAsyncInternal<TPage, TViewModel>(navigationParams, animated, false);

public Task PushModalAsync<TPage, TViewModel>(bool animated = true)
    where TPage : Page
    where TViewModel : ViewModelBase
    => PushAsyncInternal<TPage, TViewModel>(null, animated, true);

public Task PushModalAsync<TPage, TViewModel>(Dictionary<string, object> navigationParams, bool animated = true)
    where TPage : Page
    where TViewModel : ViewModelBase
    => PushAsyncInternal<TPage, TViewModel>(navigationParams, animated, true);
private async Task PushAsyncInternal<TPage, TViewModel>(Dictionary<string, object> navigationParams = null, bool animated = true, bool modal = false)
    where TPage : Page
    where TViewModel : ViewModelBase
{
    Page page = await CurrentApplication.CreatePage<TPage, TViewModel>(navigationParams);

    if (modal)
    {
        await Navigation.PushModalAsync(page, animated);
    }
    else
    {
        await Navigation.PushAsync(page, animated);
    }
}

Now that our implementation is in place, usage of this feature would be as follows.

public class ViewModelA : ViewModelBase
{
    public async Task NavigateToPageB()
    {
        var p = new Dictionary<string, object>()
        {
            ["Key"] = "value"
        };
        await PushAsync<ViewB, ViewModelB>(p);
    }
}

public class ViewModelB : ViewModelBase
{
    public override async Task Initialize(Dictionary<string, object> navigationsParams = null) 
    {
        if (navigationsParams?.ContainsKey("key") ?? false)
        {
            // something super important is going on here...
        }
    }
}

Bonus: Navigation Parameters on Pop

Adding support for navigation parameters when popping is relatively simple as it follows the same pattern that we just implemented. In this example, we’ll just handle the case of PopAsync and not PopModalAsync or PopToRootAsync. Although, those would follow the same approach.

First, we define the method on our ViewModelBase to override to respond to being popped to.

public virtual async Task PoppingTo(Dictionary<string, object> navigationsParams = null) => await Task.CompletedTask;

Next, we would update our pass-through method PopAsync and ViewModelBase to handle passing these navigation parameters. A unique check we should do here is to ensure that we have another ViewModelBase in the stack to pop to.

public Task<Page> PopAsync(Dictionary<string, object> navigationParams = null, bool animated = true) 
{
    if (NavigationStack.Count > 1)
    {
        var vm = NavigationStack[NavigationStack.Count - 2].BindingContext as ViewModelBase;

        if (vm != null)
        {
            await vm.PoppingTo(navigationParams);
        }
    }

    Page popped = await Navigation.PopAsync(animated);

    return popped;
}

That’s it! Usage of this feature would be as follows.

public class ViewModelB : ViewModelBase
{
    public async Task GoBack() 
    {
        var p = new Dictionary<string, object>()
        {
            ["Key"] = "value"
        };
        await PopAsync(p);
    }
}

public class ViewModelA : ViewModelBase
{
    public override async Task PoppingTo(Dictionary<string, object> navigationsParams = null)
    {
        // something important is going on
    }
}

You can find the entire project on Github:
https://github.com/jacob-maristany/Xamarin.Forms.Mvvm


Next time on…

In Part II of this series, we’ll build on what we’ve learned to add support to our responsive View Model for handling responses to view appearing/disappearing events.

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.