Navigation for Windows Phone 7 using MVVM

by Geert 31. March 2011 10:32

While we were working on the support for Windows Phone 7 in Catel, we encountered a complex problem about navigation inside WP7 from within view-models.

As you probably know by know, the view-models in Catel support services to handle message boxes, UI dialogs (not in WP7, but in Silverlight and WPF), running processes (WPF only) and more. After a good night of sleep, it turned out that the navigation service will look a bit like the IUIVisualizerService that already exists in Catel.

But we are not there yet, there are some “side-effects” that we need to keep in mind with Windows Phone 7:

  • You cannot pass arguments using default navigation
    The only way to pass arguments is via query parameters (url.xaml?parameter=value)
  • Care about tomb stoning
    When tomb stoning, a page is navigated to again (so we can’t instantiate a view-model in view X and go to Y, and then tomb stone the app in Y, you won’t have your view-model again

So, how can we solve this problem? In the end, when you have the solution at hand, it is always easy. But what we wanted to accomplish with the navigation service in Catel is that you would be able to pass parameters to other view-models without actually having to care about the NavigationContext to request the parameters.

Introducing parameters (navigation context) in ViewModelBase

We used to following code to “upgrade” the ViewModelBase class in WP7:

   1: /// <summary>
   2: /// Gets the navigation context.
   3: /// </summary>
   4: /// <value>The navigation context.</value>
   5: /// <remarks>
   6: /// Note that the navigation contexts is first available in the <see cref="Initialize"/> method, 
   7: /// not in the constructor.
   8: /// </remarks>
   9: protected Dictionary<string, string> NavigationContext { get { return _navigationContext; } }

The _navigationContext is an private dictionary containing the actual keys available in the NavigationContext of the PhoneApplicationPage the view-model is available for.

The navigation context can be updated via an internal method:

   1: /// <summary>
   2: /// Updates the navigation context. The navigation context provided by this class is different
   3: /// from the <see cref="NavigationContext"/>. Therefore, this method updates the navigation context
   4: /// to match it to the values of the <param name="navigationContext"/>.
   5: /// </summary>
   6: /// <param name="navigationContext">The navigation context.</param>
   7: internal void UpdateNavigationContext(NavigationContext navigationContext)
   8: {
   9:     lock (_navigationContext)
  10:     {
  11:         _navigationContext.Clear();
  12:  
  13:         if (navigationContext != null)
  14:         {
  15:             foreach (string key in navigationContext.QueryString.Keys)
  16:             {
  17:                 _navigationContext.Add(key, navigationContext.QueryString[key]);
  18:             }
  19:         }
  20:     }
  21: }

Then, in the OnLoaded event of the PhoneApplicationPage, we update the navigation context (but only if it is a ViewModelBase of Catel:

   1: if (_viewModel is ViewModelBase)
   2: {
   3:     ((ViewModelBase)_viewModel).UpdateNavigationContext(NavigationContext);
   4: }

Using parameters in a view model

Now we have a view-model that has support for parameters, let’s take a look how easy it is to use them. In an implementation of the ViewModelBase, the query parameters are available in the Initialize method:

   1: /// <summary>
   2: /// Initializes the object by setting default values.
   3: /// </summary>    
   4: protected override void Initialize()
   5: {
   6:     int shopIndex = -1;
   7:     if (NavigationContext.ContainsKey("ShopIndex"))
   8:     {
   9:         int.TryParse(NavigationContext["ShopIndex"], out shopIndex);
  10:     }
  11:  
  12:     if (shopIndex != -1)
  13:     {
  14:         _existingShop = true;
  15:         Shop = UserData.Instance.Shops[shopIndex];
  16:     }
  17:     else
  18:     {
  19:         Shop = new Shop();
  20:     }
  21: }

As you see, the navigation parameter is passed as an index of a list. Normally, you would pass a unique object ID that allows you to fetch the object (model). In this view-model, we choose to either fetch an existing shop, or create a new one.

Using the INavigationService

We know that we have a view-model that supports parameters, so let’s take a look at the actual navigation service. I don’t want to scare you off, but also want to show you all the capabilities of the INavigationService, so here is the full interface declaration:

   1: public interface INavigationService
   2: {
   3:     /// <summary>
   4:     /// Gets a value indicating whether it is possible to navigate back.
   5:     /// </summary>
   6:     /// <value>
   7:     ///     <c>true</c> if it is possible to navigate back; otherwise, <c>false</c>.
   8:     /// </value>
   9:     bool CanGoBack { get; }
  10:  
  11:     /// <summary>
  12:     /// Gets a value indicating whether it is possible to navigate forward.
  13:     /// </summary>
  14:     /// <value>
  15:     ///     <c>true</c> if it is possible to navigate backforward otherwise, <c>false</c>.
  16:     /// </value>
  17:     bool CanGoForward { get; }
  18:  
  19:     /// <summary>
  20:     /// Navigates back to the previous page.
  21:     /// </summary>
  22:     void GoBack();
  23:  
  24:     /// <summary>
  25:     /// Navigates forward to the next page.
  26:     /// </summary>
  27:     void GoForward();
  28:  
  29:     /// <summary>
  30:     /// Navigates to a specific location.
  31:     /// </summary>
  32:     void Navigate(string uri);
  33:  
  34:     /// <summary>
  35:     /// Navigates to a specific location.
  36:     /// </summary>
  37:     /// <param name="uri">The URI.</param>
  38:     /// <param name="parameters">Dictionary of parameters, where the key is the name of the parameter, 
  39:     /// and the value is the value of the parameter.</param>
  40:     void Navigate(string uri, Dictionary<string, object> parameters);
  41:  
  42:     /// <summary>
  43:     /// Navigates to a specific location.
  44:     /// </summary>
  45:     void Navigate(Uri uri);
  46:  
  47:     /// <summary>
  48:     /// Navigates the specified location registered using the view model type.
  49:     /// </summary>
  50:     /// <typeparam name="TViewModelType">The view model type.</typeparam>
  51:     void Navigate<TViewModelType>();
  52:  
  53:     /// <summary>
  54:     /// Navigates the specified location registered using the view model type.
  55:     /// </summary>
  56:     /// <typeparam name="TViewModelType">The view model type.</typeparam>
  57:     /// <param name="parameters">Dictionary of parameters, where the key is the name of the parameter, 
  58:     /// and the value is the value of the parameter.</param>
  59:     void Navigate<TViewModelType>(Dictionary<string, object> parameters);
  60:  
  61:     /// <summary>
  62:     /// Navigates the specified location registered using the view model type.
  63:     /// </summary>
  64:     /// <param name="viewModelType">The view model type.</param>
  65:     void Navigate(Type viewModelType);
  66:  
  67:     /// <summary>
  68:     /// Navigates the specified location registered using the view model type.
  69:     /// </summary>
  70:     /// <param name="viewModelType">The view model type.</param>
  71:     /// <param name="parameters">Dictionary of parameters, where the key is the name of the parameter, 
  72:     /// and the value is the value of the parameter.</param>
  73:     void Navigate(Type viewModelType, Dictionary<string, object> parameters);
  74:  
  75:     /// <summary>
  76:     /// Registers the specified view model and the page type. This way, Catel knowns what
  77:     /// page to show when a specific view model page is requested.
  78:     /// </summary>
  79:     /// <param name="viewModelType">Type of the view model.</param>
  80:     /// <param name="pageType">Type of the page.</param>
  81:     /// <exception cref="ArgumentException">when <paramref name="viewModelType"/> does not implement <see cref="IViewModel"/>.</exception>
  82:     /// <exception cref="ArgumentException">when <paramref name="pageType"/> is not of type <see cref="PhoneApplicationPage"/>.</exception>
  83:     void Register(Type viewModelType, Type pageType);
  84:  
  85:     /// <summary>
  86:     /// Registers the specified view model and the page type. This way, Catel knowns what
  87:     /// page to show when a specific view model page is requested.
  88:     /// </summary>
  89:     /// <param name="name">Name of the registered page.</param>
  90:     /// <param name="pageType">Type of the page.</param>
  91:     /// <exception cref="ArgumentException">when <paramref name="name"/> is <c>null</c> or empty.</exception>
  92:     /// <exception cref="ArgumentException">when <paramref name="pageType"/> is not of type <see cref="PhoneApplicationPage"/>.</exception>
  93:     void Register(string name, Type pageType);
  94:  
  95:     /// <summary>
  96:     /// This unregisters the specified view model.
  97:     /// </summary>
  98:     /// <param name="viewModelType">Type of the view model to unregister.</param>
  99:     /// <returns>
 100:     ///     <c>true</c> if the view model is unregistered; otherwise <c>false</c>.
 101:     /// </returns>
 102:     bool Unregister(Type viewModelType);
 103:  
 104:     /// <summary>
 105:     /// This unregisters the specified view model.
 106:     /// </summary>
 107:     /// <param name="name">Name of the registered page.</param>
 108:     /// <returns>
 109:     ///     <c>true</c> if the view model is unregistered; otherwise <c>false</c>.
 110:     /// </returns>
 111:     bool Unregister(string name);
 112: }

It might look complex, but it’s not. Most of the methods are about navigating to a specific uri, with or without arguments.

The basic method is the Navigate(uri, parameters) method. Every other method calls this instance which will convert the uri and parameters into a valid html encoded request string and pass it to the Navigate(Uri) method eventually.

Thus, a simple example on how to use the service inside a view-model is this:

   1: var navigationService = GetService<INavigationService>();
   2: navigationService.Navigate("/UI/Pages/ShopPage.xaml");

And, if you want to use parameters:

   1: var parameters = new Dictionary<string, object>();
   2: parameters.Add("ShopIndex", Shops.IndexOf(SelectedShop));
   3:  
   4: var navigationService = GetService<INavigationService>();
   5: navigationService.Navigate("/UI/Pages/ShopPage.xaml", parameters);

This is not all. If you have studied the interface carefully, you might have seen the Navigate<TViewModel> method. This is a very special method that we also used in the IUIVisualizerService. In your view-model, you don’t want to navigate to a specific page, you want to run another view-model. Therefore, it is possible to navigate via view-model types instead of the actual pages. Without parameters, the code would look like this:

   1: var navigationService = GetService<INavigationService>();
   2: navigationService.Navigate<ShopViewModel>();

And again it is very easy to pass parameters to the view-model:

   1: var parameters = new Dictionary<string, object>();
   2: parameters.Add("ShopIndex", Shops.IndexOf(SelectedShop));
   3:  
   4: var navigationService = GetService<INavigationService>();
   5: navigationService.Navigate<ShopViewModel>(parameters);

Example code

Nice, but where is the code? I have decided not to include an actual project with this blog post. If you are interested in seeing how the navigation service works, just download the source code of Catel. It includes a WP7 example app (Solution folder Src\Examples\WP7) which shows much more than only the navigation service. For example, it also shows how to store complex objects with nested arrays of complex objects into the isolated storage with this simple code:

   1: UserData.Save();

I will soon write a blog post about this powerful automatic serialization in WP7 (which of course also works in Silverlight and WPF).

Links

http://catel.codeplex.com

Tags: ,

C# | Catel | Windows Phone 7 | MVVM

Pingbacks and trackbacks (2)+

Comments are closed

About the Author

Geert van Horrik is an independent freelance software developer since January 1st, 2007. Since then he was been working on several projects from C++ to C# (WPF, Silverlight, ASP.NET, etc). Currently he loves to write his software using WPF (or Silverlight if WPF isn't an option).

Lately, Geert is spending a lot of time on Catel, a free open-source MVVM Framework for WPF and Silverlight. Actually, it's more than "just" an MVVM Framework, it's a complete application library!