In my previous post on this topic, Thinking Out Loud: Mvvm Navigation for XAML Frameworks such as Xamarin.Forms, UWP/WinUI, WPF and Uno, I explored using events emitted by a ViewModel to drive page navigation. This post will explore this concept further, employing the latest c# 9 code generator to reduce the boilerplate code that developers have to write.
Before we go on, let’s just recap of where we got to previously:
- ViewModels are independent, not knowing what’s before or after them in the navigation flow of the application
- Use events to signify when a ViewModel is complete
- ViewModel events are converted to navigation methods at an application level
The upshot is that you can have a simple ViewModel that simply raises an event to indicate that it’s complete (for example when the user clicks a submit button on a form).
public class MainViewModel
{
public event EventHandler ViewModelDone;
public async Task<int> DoSomething()
{
var rnd = new Random().Next(1000);
await Task.Delay(rnd);
if (rnd % 2 == 0)
{
ViewModelDone?.Invoke(this, EventArgs.Empty);
}
...
}
}
There were a couple of pieces of feedback following my previous post:
- Use an Observable instead of an event
- Bypass the ViewModel event completely and simply raise a message that could be handled by the application. For example a developer could attach a Behavior to a Button that would send a message to an application wide dispatcher that could determine where to navigate to based on the message type, or perhaps the parameter set.
Whilst I like the first idea of a ViewModel exposing an Observable, I think that this is an idea we’ll explore sometime in the future. Using an event is incredibly simple and gives us the clear separation we’re after. The only downside is that the add/remove handler code required for events is somewhat nasty.
The idea of using messages, and having a central dispatcher for messages, is a great idea and one that I wanted to explore. I didn’t want to change the ViewModel to have to emit a message, since again this just adds additional complexity to the ViewModel. This means that there needs to be some sort of conversion between events and messages. As you can imagine, this is simply adding more code that developers need to write in order to get everything to work.
I’ve just pointed out two areas where developers will have to write unnecessary code: add/remove event handlers and converting between events and messages. I’ll be using the c# 9 code generators to help eliminate this excess code.
TL;DR
In this post I’m not going to I walk through the complexities of events, messaging and code generation because that would make for a long post. Instead, let me walk through a scenario where we’re going to add a new page, FifthPage, to our existing application (which as you can probably guess, already has four pages). Here’s what we need:
- Add FifthPage
- Add a Button to MainPage that navigates to FifthPage when clicked
- Add Button to FifthPage that navigates back to MainPage when clicked
- Add FifthViewModel that will be the DataContext for FifthPage
- Add string property, Title, to FifthViewModel that returns a page title
- Add TextBlock to FifthPage that is bound to the Title property on the FifthViewModel.
Create Page and ViewModel
The first step is to create the FifthPage and FifthViewModel. I’ve separated out the application code into different projects, so I have my view models in a project called MvvmNavigation.Core. My pages are still in the MvvmNavigation.Shared project that was created by the Uno solution template.
Whilst we’re creating these classes, we’ll add some of the basics that we’re going to need. On the FifthPage, we’ll add a TextBlock, for the Title, and a Button, to trigger navigation back to the MainPage.
<Page
x:Class="MvvmNavigation.FifthPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel VerticalAlignment="Center"
HorizontalAlignment="Center">
<TextBlock Text="page title" />
<Button Content="Go Back" />
</StackPanel>
</Page>
In the FifthViewModel we’ll add a Title property and a simple event, FifthDone, that will indicate to the application that the FifthViewModel is done. The application will be responsible for determining how to handle this; which according to our specification, should navigate back to MainPage. We’ll also create a RaiseFifthDone method that can be called to invoke the FifthDone event.
public class FifthViewModel
{
public event EventHandler FifthDone;
public string Title => "Page 5";
public void RaiseFifthDone()
{
FifthDone?.Invoke(this, EventArgs.Empty);
}
}
Navigation to FifthPage
To navigate to the FifthPage we need a new Button on the MainPage that, when clicked, will raise a message, PleadTheFifthMessage, that the application will handle in order to navigate to the FifthPage. Let’s unpack this into the steps:
Add Button
Add a Button to the MainPage XAML, along with the NavigationMessageAction behavior which will raise the PleadTheFifthMessage
<Button Content="Go To Page 5">
<Interactivity:Interaction.Behaviors>
<Interactions:EventTriggerBehavior EventName="Click">
<builditbehaviors:NavigationMessageAction MessageType="localmessages:PleadTheFifthMessage" />
</Interactions:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</Button>
PleadTheFifthMessage
Add a class, PleadTheFifthMessage, that inherits from CompletedMessage.
public class PleadTheFifthMessage : CompletedMessage
{
public PleadTheFifthMessage() : base() { }
public PleadTheFifthMessage(object sender) : base(sender) { }
}
Map PleadTheFifthMessage to FifthViewModel
As part of the MvvmApplicationService, we need to register a navigation to the FifthViewModel for the PleadTheFifthMessage. In this case, we’re only handling the PleadTheFifthMessage for when it’s raised by the MainViewModel.
serviceRegistrations.AddSingleton<INavigationMessageRoutes>(sp =>
{
var routes = new NavigationMessageRoutes()
.RegisterNavigate<MainViewModel, PleadTheFifthMessage, FifthViewModel>()
.RegisterNavigate<MainViewModel, CompletedMessage, SecondViewModel>()
// ... omitted for brevity
.RegisterGoBack<CloseMessage>();
return routes;
});
FifthPage – FifthViewModel Mapping
Whilst we’ve defined a mapping from the PleadTheFifthMessage to the FifthViewModel, there needs to be a way for the application to connect the FifthViewModel to the FifthPage. Rather than rely on naming convention, we’re going to apply an Attribute to the FifthPage.
[ViewModel(typeof(FifthViewModel))]
public sealed partial class FifthPage
{
public FifthPage()
{
this.InitializeComponent();
}
}
ViewModel Binding
So far we’ve wired up the navigation from MainPage to FifthPage. However, when arriving at FifthPage it’s clear than neither the Title, nor the Button event handler, has been wired up. The title could be easily wired up by simply creating an instance of the FifthViewModel in XAML and setting it as the DataContext.
However, this doesn’t scale well for real world applications where a view model may be dependent on any number of services that are required (eg for fetching and/or saving data). It’s preferable to have some sort of depedency injection framework that can be used to instantiate the view model.
ViewModel Instantiation
To this end, we’re going to add a ViewModel property to our FifthPage, along with a partial method, InitiViewModel (note also that we’ve added a second parameter to the ViewModel attribute). The implementation of the partial method will be done by our code generator that will generate the code necessary to instantiate, along with any dependent services, the FifthViewModel.
[ViewModel(typeof(FifthViewModel), nameof(InitViewModel))]
public sealed partial class FifthPage
{
partial void InitViewModel();
public FifthViewModel ViewModel => this.ViewModel(() => DataContext as FifthViewModel, () => InitViewModel());
public FifthPage()
{
this.InitializeComponent();
}
}
FifthViewModel Registration
Despite providing the mapping between FifthPage and FifthViewModel, there’s currently no way for the dependency injection container to create an instance of FifthViewModel, since we haven’t registered the FifthViewModel type. Rather than have the developer work out where to add the code to register the FifthViewModel type, we can simply attribute the FifthViewModel:
[Register]
public class FifthViewModel
{
// ...
}
Bind FifthViewModel With x:Bind
We can then update the XAML of our FifthPage to data bind both the Title property and the RaiseFifthDone method on the FifthViewModel.
<Page x:Class="MvvmNavigation.FifthPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel VerticalAlignment="Center"
HorizontalAlignment="Center">
<TextBlock Text="{x:Bind ViewModel.Title}" />
<Button Content="Go Back"
Click="{x:Bind ViewModel.RaiseFifthDone}" />
</StackPanel>
</Page>
Navigation Back to MainPage
When we created the FifthViewModel we already created the FifthDone event which will be invoked by the RaiseFifthDone method. However, clicking the Button on the FifthPage currently does nothing – actually it does indeed raise the FifthDone event but currently nothing is listening to that event.
Let’s add the EventMessage attribute to the FifthDone event. In this case the attribute references the existing CloseMessage which is the message that will be dispatched when the FifthDone event is raised.
[Register]
public class FifthViewModel
{
[EventMessage(typeof(CloseMessage))]
public event EventHandler FifthDone;
// ...
}
We already have a handler for the CloseMessage for all pages that will simply close the current page.
That completes all the steps necessary to add a new page along with navigation too and from the page. It seems there’s a lot of steps, but actually there’s only minimal code required and it facilitates a high degree of separation between the elements of the application.
If you want to check out the code for this example app, feel free to check out the MvvmNavigation GitHub repo. Note that this is a work in progress and that there will most likely be quite a bit of refactoring over the coming weeks, after which I’ll post about more of the details on how the various mappings work and the code generation that’s used behind the scenes.