
One of the things that bothered me in the examples of Windows Phone 7 applications was the way the URLs of the pages were scattered throughout the application. As soon as you rename a page or or move it to a different folder or rename a parameter you need to search for the old URLs and replace them which can be forgotten or you could miss a couple.
In order to fix this I came up with this class:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Windows.Controls; namespace Example.Providers { internal class Navigator { private readonly Dictionary<type string ,> _navigationMap = new Dictionary<type string ,>(); internal Navigator() { var viewsFolder = "/Views/"; _navigationMap.Add( typeof(Views.CourseDetails), viewsFolder + "CourseDetails.xaml?courseCode={0}"); _navigationMap.Add( typeof(Views.Courses), viewsFolder + "Courses.xaml?searchString={0}"); _navigationMap.Add( typeof(Views.Help), viewsFolder + "Help.xaml"); _navigationMap.Add( typeof(Views.LocationNavigation), viewsFolder + "LocationNavigation.xaml?locationName={0}"); _navigationMap.Add( typeof(Views.Search), viewsFolder + "Search.xaml"); _navigationMap.Add( typeof(Views.Locations), viewsFolder + "Locations.xaml"); _navigationMap.Add( typeof(Views.SubCategories), viewsFolder + "SubCategories.xaml?categoryId={0}"); } internal void Navigate(Page currentPage, Type page, params object[] arguments) { Debug.Assert(currentPage != null); // BUGFIX: prevent resize/flickering during // transition by delaying the execution of the navigation // See: http://forums.create.msdn.com/forums/t/83237.aspx currentPage.Dispatcher.BeginInvoke(()=> { currentPage.NavigationService.Navigate( new Uri( string.Format(_navigationMap[page], arguments), UriKind.Relative)); }); } } }This Navigator class stores the URLs to the pages, including any arguments in a Dictionary. The Navigate method allows you to pass the current page and the type of the page you want to navigate to and optionally the arguments that should be passed to the page.
I also used the Navigate method to implement a bug fix for the annoying flickering of the pages during navigation.
Using the Navigator class is easy: I added a property and a Navigate method to my App class:
private Navigator _navigator; private void Application_Launching(object sender, LaunchingEventArgs e) { _navigator = new Navigator(); } private void Application_Activated(object sender, ActivatedEventArgs e) { _navigator = new Navigator(); } internal void Navigate(Page fromPage, Type toPage, params object[] arguments) { _navigator.Navigate(fromPage, toPage, arguments); }I added a NavigateTo method to the base class of my Views:
internal void NavigateTo(Type page, params object[] arguments) { ((App)(App.Current)).Navigate(this, page, arguments); }This way I can easily access the Navigator from any View in my application:
private void ShowMapButton_Click(object sender, RoutedEventArgs e) { var frameworkElement = sender as FrameworkElement; if (frameworkElement != null) { var location = frameworkElement.DataContext as Example.ViewModels.Location; if (location != null) { NavigateTo(typeof(Views.LocationNavigation), location.Name); } } }The View has its DataContext set to a ViewModel (Location) so in the click event of the button I retrieve the ViewModel and pass one the properties (Name) as a parameter to the Navigate method.
To accommodate the easy retrieval of parameters I also added two methods to my View base class:
protected bool TryGetQueryString(string key, out int value) { string valueString; if (NavigationContext.QueryString.TryGetValue(key, out valueString)) { int i; if (Int32.TryParse(valueString, out i)) { value = i; return true; } } value = 0; return false; } protected bool TryGetQueryString(string key, out string value) { string valueString; if (NavigationContext.QueryString.TryGetValue(key, out valueString)) { value = valueString; return true; } value = null; return false; }These two methods allow the code in the Views to be readable and pretty short:
using System.Windows; using Microsoft.Phone.Controls.Maps; namespace ISTraining.Views { public partial class LocationNavigation : PageBase { protected override void LoadDataContext() { ViewModels.LocationNavigation locationNavigation = null; string locationName; if (TryGetQueryString("locationName", out locationName)) { locationNavigation = new ViewModels.LocationNavigation(); locationNavigation.LoadCommand.Execute(locationName); } this.DataContext = locationNavigation; } } }The LoadDataContext method is called by the View’s base class when the page needs to load the DataContext and can’t get it from its cache.
The only two things I would like to see fixed are:
- Being able to navigate from within a ViewModel class. Due to the fact that a navigation requires the current View I would have to pass a reference to the current View to the ViewModel which feels ugly and out of place.
- I still need to register the correct path to each View. I haven’t figured out yet how to get the path using reflection or a resource manager.
- I am considering replace/sub classing the hyperlink button to make it compatible with this solution.
When and if I find solutions to these issues I’ll post them.
Share this
6 comments
on navigating from a viewmodel, I define a static event in the viewmodel then handle it in the view. not exactly an enforceable pattern like a base class or interface, but it does keep things where they belong.
burton
Burton, yes that is an option too. It still feels wrong because it is neither the ViewModel’s responsibility to navigate (it is to initiate it) nor the View’s. That is why I use a Message broker in WPF and regular Silverlight and have an independent ViewHandler class. The problem seems to be that the Navigate method of the Navigation Service requires the current view.
Erno de Weerd
Please see this solution: http://www.sharpregion.com/easy-windows-phone-7-navigation/
It makes it possible to navigate and pass any type of data like this:(data);
NavigationService.Navigate
And then to get the data like this:();
var data = NavigationContext.GetData
Adrian
Adrian, yes I can see how it works and how it simplifies passing the url around. I solved the GUID and navigation service reference by using the View base class and the App class.
Erno de Weerd
Erno, please notice that NavigationService is a property of the page. Being a property (well, an instance of some type) – we can create an extension method for it.
Having the extension method, the solution is generic and accessible from all pages without the need for a base class.
Adrian
Yes, I did notice that but I trying to have no code in my Views. So I really want the navigation to be executed in my viewmodels.
Erno de Weerd