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.