Condividi tramite


Silverlight Navigation With the MVVM Pattern

I recently had a query from a customer that was one of those ones that you think “Aha! That’s easy, you just do this”. Then you think a bit more about it and realise that if you want to do it properly it’s not quite as simple as you first thought.

This particular query related to how to use the navigation framework introduced in Silverlight 3 with the MVVM pattern. For those that don’t know the Model-View-ViewModel pattern is a common pattern often used to build Silverlight and WPF applications since it has strong support for databound UIs and provides good abstraction for unit testing and keeping your view logic separate from the view and the model.

Enter the Silverlight Navigation Framework

Handling the page style navigation that we’ve all become used to on the web can be a real pain in RIAs written using Silverlight, Flash or AJAX involving lots of tracking of page clicks using HTML bookmarks and some liberal use of JavaScript. Fortunately Silverlight 3 added a navigation framework to help with this.

This framework works by you adding a Frame element to your root XAML and then creating many Page derived classes. These classes can then navigate between each other by calling into a NavigationService instance that each page inherits from it’s base class.

The issue with MVVM is that page navigation is a view logic, that means that it should sit inside the ViewModel, however the NavigationService is only on the Page class which the ViewModel doesn’t have access to.

Solving this problem is simple, just pass the NavigationService instance to the ViewModel, job done! Well ok, that works if you hate unit testing. If you like unit testing though you may find you have problems then when it comes to testing your ViewModel as you won’t have a NavigationService instance to pass it.

Additionally since many pages are likely to want to do this I’d like to have a reusable approach that doesn’t take much code to reuse.

The Solution

The solution I came up with is one of a few different ways you could implement this but this works for me at the moment. Feel free to point out any glaring errors in my design though.

Wrapping the NavigationService

First I need to make the NavigationService mockable so I can use it in unit tests. To do this I created a new interface INavigationService that exposes the methods and properties of the NavigationService. This sample version only exposes Navigate() but you could easily expose more as needed.

1 public interface INavigationService
2 {
3     void Navigate(string url);
4 }

Not the most complex interface ever devised you’ll agree.

Next I created a class that implemented the interface and took a reference to a System.Windows.Navigation.NavigationService on it’s constructor.

01 public class NavigationService : INavigationService
02 {
03     private readonly System.Windows.Navigation.NavigationService _navigationService;
04  
05     public NavigationService(System.Windows.Navigation.NavigationService navigationService)
06     {
07         _navigationService = navigationService;
08     }
09  
10     public void Navigate(string url)
11     {
12         _navigationService.Navigate(new Uri(url, UriKind.RelativeOrAbsolute));
13     }
14 }

Supporting INavigationService in the ViewModel

Now I had an abstraction I needed to pass the INavigationService to the ViewModel. I wanted to do this in a standard way. I could have made it a constructor argument but I couldn’t always guarantee I’d be there at construction. The best way seemed to be to add a property. I decided to put that property on an interface so I had a defined contract that ViewModels could support.

INavigable.

1 public interface INavigable
2 {
3     INavigationService NavigationService { getset; }
4 }

This interface provides a single property that a ViewModel can implement that will contain a reference to the INavigationService that the ViewModel should use to perform navigation when it needs to.

Passing the INavigationService to the ViewModel

Next I need to create an instance of the NavigationService that wraps the System.Windows.Navigation.NavigationService in the Page class and pass that to the ViewModel. I’d like this to be reusable code and if possible I don’t want any code behind in my View.

This is a perfect use for an attached behaviour. What’s one of those? It’s simply an attached property with a property changed handler on it that hooks up code to the DependencyObject that the property gets attached to. It’s simpler than it sounds and is a nice way of making reusable logic that you want to attach to objects in XAML.

The Navigator.

01 public static class Navigator
02 {
03     public static INavigable GetSource(DependencyObject obj)
04     {
05         return (INavigable)obj.GetValue(SourceProperty);
06     }
07  
08     public static void SetSource(DependencyObject obj, INavigable value)
09     {
10         obj.SetValue(SourceProperty, value);
11     }
12  
13    public static readonly DependencyProperty SourceProperty =
14         DependencyProperty.RegisterAttached("Source"typeof(INavigable), typeof(Navigator), new PropertyMetadata(OnSourceChanged));
15  
16     private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
17     {
18         Page page = (Page) d;
19  
20         page.Loaded += PageLoaded;
21     }
22  
23     private static void PageLoaded(object sender, RoutedEventArgs e)
24     {
25         Page page = (Page)sender;
26  
27         INavigable navSource = GetSource(page);
28  
29         if (navSource != null)
30         {
31             navSource.NavigationServicenew NavigationService(page.NavigationService);
32         }
33     }
34 }

This class provides a single attached property definition of the type INavigable. This property has a handler that when invoked grabs the DependencyObject you are attaching the property to and hooks up it’s Loaded event.

So when you attach this property to a Page instance in XAML it will fire off the PageLoaded method when the Page you attach it to loads. In the page load handler I then query the Page instance for the Source attached property. Remember that this property is of type INavigable. If the source supports INavigable I then create a new instance of the NavigationService, wrapping the Page’s instance and set it on the source using the INavigable.NavigationService property.

And there we have it, a reusable way of attaching the navigation service instance for a Page to the Page’s view model.

The View XAML

And finally using the attached property is a case of assigning the ViewModel to the DataContext of the Page as normal and then doing this.

01 <navigation:Page x:Class="SLNavigation.Page1"
02            xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
03            xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
04            xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
05            xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
06            mc:Ignorable="d"
07            xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
08            xmlns:SLNavigation="clr-namespace:SLNavigation"
09            d:DesignWidth="640" d:DesignHeight="480"
10            Title="Page1 Page"
11            SLNavigation:Navigator.Source="{Binding}"
12     >

Summary

So that’s it, my solution. I think it’s reasonable, it minimised the amount of code needed in the View code behind to nil, makes the only code you need on the ViewModel is to derive from and implement INavigable which is one property and to put an attached property on the Page in the XAML. It keeps a good separate of concerns as the ViewModel is still unit testable but it can now support navigation.

Let me know what you think.

Originally posted by Robert Garfoot on 8th September 2010 here: https://garfoot.com/blog/2010/09/silverlight-navigation-with-the-mvvm-pattern/

Comments

  • Anonymous
    October 11, 2010
    The comment has been removed

  • Anonymous
    October 18, 2010
    If you are getting a null reference exception when accessing the NavigationService then it’s likely you’ve missed the attached property in the view’s XAML to set up the Navigator.Source. This attached property hooks up to the INavigable interface on the view model. I’ve uploaded a sample implementation to my blog now so check that out.

  • Anonymous
    November 15, 2010
    Looks good, but I would like to see some examples of how we actually change the page within a view model.  For example, my app has a Main page that has the frame that contains the child navigation pages.  The Main page has its own ViewModel, as does each of the child navigation pages.  How would I change pages from any nav page to any other nav page?  Does the page-change code need to be in the MainViewModel, or can it be in any ViewModel? Thanks, Ken

  • Anonymous
    December 06, 2010
    The navigator attached property is per page and can only bind to a single INavigable implementation. This isn’t typically an issue for most pages as they only have a single view model. Each page / viewmodel pair should have its own navigator/INavigable pair. The complexity increases when dealing with pages that have child view models that require navigation, however most likely the child view models are created by the parent so it can simply pass it’s NavigationService instance through into the child view models. How you do that is up to you, you could pass a Func<> into the children that then can use to query back to the parent NavigationService or you could make the child view models support INavigable too and have the parent view model update them accordingly. The simplest option would be to implement each child navigation page as a separate view, then each view has its own view model and it’s a 1:1 pairing again avoiding child view models so the standard Navigator/INavigable trick should work.