Windows Phone 7 Navigation
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.