Creating a reusable base for WP7 apps - Navigation

Yngve Bakken Nilsen
csharp WP7 boilerplate navigation

Navigation

In Part 1 we covered the basic stuff like ViewModels, setting up the project structure. In this part we'll move forward, and see how we can implement navigation and commands in our application

Navigation the old fashioned way

Normally when navigating in a Windows Phone 7 application we utilize the NavigationService in the xaml.cs file for the page, probably in some event handler. Let's do that right now, and take a look at what's good and what's bad about it

Create a new xaml
Add a new empty Portrait page, and let's call it SubPage.xaml:

Now go back to MainPage.xaml, and add a button in the ContentPanel Grid:

  
        <!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <Button x:Name="GoToSubpage" Content="Go to subpage" />
        </Grid>
 

This should add a button that takes up the entire page. Double click the button in the designview so that we get a click event generated:

  
        private void GoToSubpage_Click(object sender, RoutedEventArgs e)
        {

        }

Okay, so suppose we want to navigate to the subpage, we would have to add the following line to our newly created event:

  
        private void GoToSubpage_Click(object sender, RoutedEventArgs e)
        {
            NavigationService.Navigate(new Uri("/SubPage.xaml", UriKind.Relative));
        }

Run the application, and watch as the page changes from our Kick Ass page to the SubPage.xaml.

This is a great way of navigating through the pages, as it resembles web, and it also does all the instansiating for us. What it doesn't cover is how to assign our ViewModel and setting the DataContext and so on. Especially if your application grows, and you get alot of views, you'll find yourself repeating yourself over and over. That's what we would like to avoid.

In order to fix this, and make it more reusable we'll define a convension/fact/constraint (or what have you):

One XAML page shall have one, and only one, ViewModel class

With this in place we can also deduct that one ViewModel only has one xaml Page related to it.
We should be able to utilize this, and by looking at the previous couple of lines, we'll change the way we navigate from change xaml to

Navigating from Page1.xaml to Page2.xaml happens by changing the ViewModel

 

The PageDefinitionAttribute class

To achieve this, we'll need to write some code, and also tend to some good'ol reflection (it's not as scary as you might think)

Let's start off by implementing the attribute, and explaining what it should do:
What I would like to achieve, is a way to have my ViewModel look like this:

  
    [PageDefinition("/MainPage.xaml")]
    public class MainPageViewModel: ViewModel
    {
        public MainPageViewModel()
        {
            Title = "Kick Ass";
        }

        public string Title { get; private set; }
    }

Writing an attributeclass like this is a walk in the park, and the only code we need is this:

  
    [AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
    public class PageDefinitionAttribute : Attribute
    {
        public string RelativePath { get; set; }

        public PageDefinitionAttribute(string relativePath)
        {
            RelativePath = relativePath;
        }
        public Uri RelativeUri
        {
            get
            {
                return new Uri(RelativePath, UriKind.Relative);
            }
        }
    }

As you see, I'm passing in the relative path of the Page the ViewModel relates to, and then exposing this as a RelativeUri (that the NavigationService can use to navigate on). I'm also constraining it to classes, and making sure only one instance per class is allowed. I've put this attribute in a folder called Core, but you're free to do whatever you like :)

The next thing I'd like to be able to do is to use the MasterViewModel to handle the navigation within my app. It's always there, and it also controls the current ViewModel. Since we're tying these together, it seems like a natural place to put it. The method I'm going to implement is this:

  
    public void Goto<T>()
    {

    }

The idea here is to call it like App.ViewModel.Goto<SubPageViewModel>(); - And that's what we'll achieve by using some reflection.

The steps we need to complete in order to find out everything we need in order to navigate are:

  • Get the passed in type
  • Retrieve the PageDefinitionAttribute
  • Navigate to the RelativeUrl defined on the attribute
  • Instansiate the correct ViewModel when the page is navigated to

Getting the passed in type and retrieving the PageDefinitionAttribute
Getting the type of the passed in type is as simple as var type = typeof(T);, so that's pretty straight forward. As for the PageDefinitionAttribute, I'll implement that piece of code in an extension method that extends Type:

 

  
    public static class ViewModelExtensions
    {
        public static PageDefinitionAttribute PageDefinition(this Type modelType)
        {
            var attribute = modelType.GetCustomAttributes(true).OfType<PageDefinitionAttribute>().FirstOrDefault();
            if (attribute != null) return attribute;
            throw new InvalidOperationException(String.Format("PageDefinition is missing on {0}", modelType.Name));
        }
    }

This is pretty straight-forward reflection, and all I'm doing is getting the attributes of type PageDefinition and picking the FirstOrDefault. If it's non-existant it means we're trying to navigate to a ViewModel that has no View associated with it, hence an Exception.

With this in place, our Goto<T>() method looks like this:

  
    public void Goto<T>()
    {
        var type = typeof (T);
        var pageDefinition = type.PageDefinition();
    }

This is great, and we can now get the Uri from pageDefinition. The next thing we need is access to the NavigationService, and luckily we have the CurrentPage defined on our MasterViewModel, so let's just implement the NavigationService as a auto property:

  
    public NavigationService Navigation
    {
        get { return CurrentPage.NavigationService; }
    }

This will enable us to finish up our Goto<T>() method:

  
    public void Goto<T>()
    {
        var type = typeof (T);
        var pageDefinition = type.PageDefinition();
        Navigation.Navigate(pageDefinition.RelativeUri);
    }

Let's make a simple ViewModel for our SubPage and see if this works:

  
    [PageDefinition("/SubPage.xaml")]
    public class SubPageViewModel : ViewModel
    {

    }

And let's change the event handler in MainPage.xaml.cs to use our new method:

  
    private void GoToSubpage_Click(object sender, RoutedEventArgs e)
    {
        App.ViewModel.Goto<SubPageViewModel>();
    }

Running the app, and clicking the button should now still navigate you to the SubPage as expected. This is great! Only one thing remains when it comes to the ViewModels and Navigation, and that's setting the correct CurrentViewModel and DataContext.
Since we implemented the NavigationService in our MasterViewModel we can bind to the Navigated event, and resolve the correct ViewModel there:

  
    public MasterViewModel()
    {
        Navigation.Navigated += new NavigatedEventHandler(Navigation_Navigated);
        CurrentViewModel = new MainPageViewModel();
        CurrentPage.DataContext = CurrentViewModel;
    }

    void Navigation_Navigated(object sender, NavigationEventArgs e)
    {

    }

So let's consider what we need to do in this EventHandler:

  • Figure out the URI we've navigated to
  • Resolve the correct ViewModel for that View
  • Set the CurrentPage.DataContext to the new ViewModel

The reflection part of this is a bit heavier than in the PageDefinitionAttribute helper class, but it's still not very complicated (again with the helpers):

 

  
public static class PageHelpers  
{
    public static ViewModel FindCurrentView(Uri uri)
    {
        var type = TryToFindPageWithReflection(uri);
        return Activator.CreateInstance(type) as ViewModel;
    }

    private static Type TryToFindPageWithReflection(Uri uri)
    {
        var page = FindSubClassesOf<ViewModel>().FirstOrDefault(c => c.GetCustomAttributes(true).OfType<PageDefinitionAttribute>().Any(p => p.RelativeUri == uri));
        return page;
    }
    public static IEnumerable<Type> FindSubClassesOf<TBaseType>()
    {
        var baseType = typeof(TBaseType);
        return  Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(baseType));
    }
}

What we're doing here is first finding all the classes in the current assembly that inherits from ViewModel, then filtering them on the one that has a PageDefinitionAttribute with the passed in Uri. We should now be able to implement this in our EventHandler:

  
    void Navigation_Navigated(object sender, NavigationEventArgs e)
    {
        var viewModel = PageHelpers.FindCurrentView(e.Uri);
        CurrentPage.DataContext = viewModel;
    }

Let's test this by adjusting our SubPage.xaml and SubPageViewModel a bit. We'll add the same binding in the xaml as we did in Part 1:

        <!--TitlePanel contains the name of the application and page title-->  
        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock x:Name="ApplicationTitle" Text="MY APPLICATION" Style="{StaticResource PhoneTextNormalStyle}"/>
            <TextBlock x:Name="PageTitle" Text="{Binding Title}" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
        </StackPanel>

And then we'll add a property on our SubPageViewModel:

  
[PageDefinition("/SubPage.xaml")]
public class SubPageViewModel: ViewModel  
{
    public SubPageViewModel()
    {
        Title = "Kick Ass";
    }

    public string Title { get; private set; }
}

Run the application, and click the button, and behold:

comments powered by Disqus