Following our last post, we now have a solid framework for our app to handle state and lifecycle. However, at this point we are still navigating the app directly from the code-behind of the Views, which ties the navigation to the platform code. In addition to cluttering up our code, this also restricts us from fully taking advantage of the cross-platform opportunities offered by MvvmLight.
Today we'll see how to centralize this navigation code, removing the platform-specific definition and moving from the code-behind to the ViewModels, allowing maximum reuse of code. We’ll begin with the code related to navigation.
NavigationService
MvvmLight includes a cross-platform implementation of a navigation service which provides a device-agnostic way to perform navigation, allowing us to refactor that code into the ViewModels. The result is a fully portable, cross-platform solution.
Up until now we’ve been navigating directly against the Frame container for the application, which accepts as an argument the type of the intended page, as well as a single parameter. Obviously this implementation cannot be cross platform, since the pages are defined only in the Windows project.
Instead, the NavigationService in MvvmLight accepts a key of type string to identify the intended destination, which is resolved at runtime to the desired page. This is accomplished by first registering each page at startup, associating it with the specific key. You would do this once within each platform, reusing the same key when registering the destination so that they can be called consistently from the ViewModel.
For convenience, and to avoid typos, by convention I use the ViewModels themselves as the key, which intuitively links each one to its appropriate page. Here’s an example of the navigation registration code for our sample project:
protected INavigationService InitNavigationService()
{
var service = new NavigationService();
service.Configure(typeof(MainPageViewModel).FullName, typeof(MainPage));
service.Configure(typeof(SecondPageViewModel).FullName, typeof(SecondPage));
return service;
}
This registration needs to happen at startup so that it is available immediately throughout the project, and it’s perfectly acceptable to do this in the OnLaunched or similar event. However, since the ViewModelLocator we previously created is defined as a static resource in the App.xaml file, it is automatically instantiated and registered at application startup, so this seems like the perfect place to register the views.
Unfortunately, our current ViewModelLocator is in the portable project, obviously so that it can be leveraged on other platforms. Instead, we’ll create an inherited version of the locator and extend it with the platform-specific code to register the NavigationService and register all the pages.
Reusing the navigation code in other platforms is as simple as creating a new version of the locator for that platform and registering it with that the NavigationService implementation for that platform.
WindowsViewModelLocator
Implementing the locator for Windows is fairly simple; we only need to inherit from the BaseViewModelLocator we already defined, and of course, ensure that the inherited base constructor executes (which we need to register all the ViewModels).
In the inherited constructor, we can proceed to register the NavigationService, and any other platform-specific code that needs to be in place at startup.
We register the NavigationService by binding it to the INavigationService interface from the portable project, and pass to it a factory that initializes the service with the registered views. Here’s the complete code for the inherited locator.
public class WindowsViewModelLocator : BaseViewModelLocator
{
public WindowsViewModelLocator() : base()
{
if (ViewModelBase.IsInDesignModeStatic)
{
SimpleIoc.Default.Register<INavigationService, NavigationService>();
}
else
{
var navigationService = InitNavigationService();
SimpleIoc.Default.Register<INavigationService>(() => navigationService);
}
}
protected INavigationService InitNavigationService()
{
var service = new NavigationService();
service.Configure(typeof(MainPageViewModel).FullName, typeof(MainPage));
service.Configure(typeof(SecondPageViewModel).FullName, typeof(SecondPage));
return service;
}
}
Notice we are again checking if we are in the designer, and if so, skip the navigation registration and register the default NavigationService. This is mostly due to a bug that causes multiple instances to be registered when using a factory, resulting in the error “INavigationService is already registered”.
Skipping the navigation registration frees the designer from any clutter not necessary at design-time so it’s a good practice to follow.
The last thing we need to do with the new locator is register it as a static resource, replacing the previous base locator in App.xaml:
<Application
x:Class="Win10MvvmLight.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Win10MvvmLight"
xmlns:vm="using:Win10MvvmLight.ViewModels"
RequestedTheme="Light">
<Application.Resources>
<ResourceDictionary>
<vm:WindowsViewModelLocator x:Key="ViewModelLocator" />
</ResourceDictionary>
</Application.Resources>
</Application>
This way the registration happens automatically as soon as the app fires up.
Refactoring Navigation to the ViewModels
Now that we have a centralized, cross-platform way to navigate to a screen, we can proceed to refactor our code to the ViewModels to maximize code reuse. The first thing we need to do is add a reference to the NavigationService to the BaseViewModel class, so it is accessible throughout the app.
However, we cannot add the platform-specific NavigationService we just setup, as that version is only for Windows. Instead, we want to once again take advantage of the ServiceLocator to instead define the reference by its interface, which will be resolved at runtime to the platform-specific version.
We can do that by simply adding the following property to the BaseViewModel:
protected INavigationService NavigationService { get { return ServiceLocator.Current.GetInstance<INavigationService>(); } }
From there can call NavigateTo on the service to execute the navigation to a specified page. It would look something like this:
NavigationService.NavigateTo(typeof(SecondPageViewModel).FullName, itemId);
To make things simpler I added a generic helper method to make it even easier to call by simply specifying the type and optional parameter to the method:
public void Navigate<T>(object argument = null)
{
if (argument == null)
NavigationService.NavigateTo(typeof(T).FullName);
else
NavigationService.NavigateTo(typeof(T).FullName, argument);
}
Now that we have this in order, we can easily execute navigation from the individual ViewModels using another helpful feature of MvvmLight
RelayCommand
The RelayCommand in MvvmLight implements the ICommand interface to allow you to fire events including strongly-typed parameters, exposing them to your views to be executed by UII events (such as a button click).
A thorough discussion of Commands is outside the scope of this post, but if you are new to this concept I highly recommend you take a look at this extensive article on MSDN that goes into incredible detail on the pattern: Commands, RelayCommands and EventToCommandCommands, RelayCommands and EventToCommand.
The RelayCommand in MvvmLight can be either fire a generic method, or send a strongly typed parameter via the generic form RelayCommand. In additon, a RelayCommand can be initialized with a seperate delegate to determine whether or not a command should be allowed to execute.
This is helpful if you want to disable a command for a specific reason, such as disabling a "Refresh" button while a ViewModel is in the "Loading" state, as this would likely mean that the command is currently already executing.
We don't have any need to disable such commands in our example; we just need a way to select an item from the list on the MainPage and navigate to its details. Since this requires a specific item, we want to use the generic RelayCommand with our TestItem type so that we can use its ID property to properly navigate. Here's what the command looks like:
private RelayCommand<TestItem> selectItemCommand;
public RelayCommand<TestItem> SelectItemCommand
{
get
{
return selectItemCommand ?? (selectItemCommand = new RelayCommand<TestItem>((selectedItem) =>
{
if (selectedItem == null) return;
Navigate<SecondPageViewModel>(selectedItem.Id);
}));
}
}
Now that we have a command to navigate, we need a way to trigger it. One perfectly acceptable way to do this would be to replace our previous code-behind that fires on event-click to execute the command instead. It might look something like this:
private void listView_ItemClick(object sender, ItemClickEventArgs e)
{
var vm = DataContext as MainPageViewModel;
vm.SelectItemCommand.Execute(null);
}
While this gets the job done, there's a more elegant solution made possible by leveraging the XAML Behaviors SDK.
Behaviors SDK
The Behaviors SDK has its roots in the Expression Blend SDK that allowed such behaviors and actions in XAML. For an in-depth tour of the SDK and its features, I recommend taking a look at this this post on Behaviors SDK by Timmy Kokke, which even describes how to build your own behaviors and actions.
But for our simple project, we simply want to leverage the EventTriggerBehavior to associate a specific event -- in this case ItemClick of the ListView on the MainPage -- with the command.
First, we need to make sure we add a reference to the SDK to the project:
Then we need to add the appropriate namespaces for the behavior we want to use from the SDK, which are Interactivity and Core:
<local:ViewBase
<!-- ... -->
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
<!-- ... -->
>
At last we can proceed to use these to attach the behaviors to the ListView, associating the ItemClick event with the following XAML:
<ListView x:Name="listView"
ItemTemplate="{StaticResource TestItemTemplate}"
ItemsSource="{Binding TestItems}"
Margin="19,12,19,0"
IsItemClickEnabled="True">
<Interactivity:Interaction.Behaviors>
<Core:EventTriggerBehavior EventName="ItemClick">
<Core:InvokeCommandAction Command="{Binding SelectItemCommand}" InputConverter="{StaticResource ItemClickEventArgsConverter}"/>
</Core:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</ListView>
There's one very important property that we haven't yet covered, which is the InputConverter property. If you leave this out and attempt to call the command without converting the arguments, you'll get an error similar to this:
Unable to cast object of type 'Windows.UI.Xaml.Controls.ItemClickEventArgs' to type 'Win10MvvmLight.Portable.Model.TestItem'
The reason this happens is that the argument of the ItemClick event is of type ItemClickEventArgs, but the SelectItemCommand is expecting it to be of type TestItem. The InputConverter property lets you specify a class that will convert the arguments to the appropriate type.
This is simply an implementation if IValueConverter and in this case simply gets the clicked item out of the arguments from the click event, and passes it -- cast to the appropriate type of course -- to the command. Here's what it looks like:
public sealed class ItemClickEventArgsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var args = value as ItemClickEventArgs;
if (args == null)
throw new ArgumentException("Value is not ItemClickEventArgs");
if (args.ClickedItem is TestItem)
{
var selectedItem = args.ClickedItem as TestItem;
return selectedItem;
}
else
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
Be sure that this class is also registered as a static resource in App.xaml, so that it can be referenced in the XAML above.
With this last piece of the puzzle in place, we can now clear out ALL the code-behind from both pages, as all of the code necessary for loading the pages AND navigating between them is entirely defined in the portable project!
Wrapping Up and Next Steps
By registering our views with the NavigationService offered by Mvvm Light, we have a portable, cross-platform way to perform app navigation, separating the code from the platform-specific views. Since this service is implemented on various platforms, including Xamarin, we can use the exact same ViewModels across different devices without changing a single line of code in the portable project.
We'll come back to this in a future post, showing how we can extend this project to other platforms with Xamarin Forms. In the meantime, we'll take a break from the sample project to look closer at some of the new controls available in Windows 10, as well as how we leveraged them (and built new ones!) in our Falafel2Go app.
Until then, be sure to sign up here to get the latest version of the sample project code:
Get the CODE!
And as always, I hope this was helpful, and thanks for reading!