Tuesday, November 2, 2010

Jounce Part 5: Navigation Framework

One common request I get is how Jounce can work with the Navigation Framework.

My first reply is always, "Why do you want to use that?" As you can see in previous posts, the Jounce navigation works perfectly fine with region management to manage your needs. If you want the user to be able to "deep link" to a page, you can easily process the query string and parse it into the InitParams for the application and deal with them there.

For the sake of illustration, however, I wanted to show one way Jounce can work with an existing navigation framework. In fact, to make it easy to follow along, the quick start example works mainly from the "Navigaton Application" template provided with Silverlight.

As a quick side note, I am very much aware of the INavigationContentLoader interface. This may be the way to go and in the future I might write an adapter for Jounce, but I just don't see a compelling need to have URL-friendly links as I typically write applications that act like applications in Silverlight, not ones that try to mimic the web by having URLs.

The example here is available with the latest Jounce Source code (it's not part of an official release as of this writing so you can download it using the "Latest Version - Download" link in the upper right).

To start with, I simply created a new Silverlight navigation application.

Without overriding the default behavior, the navigation framework creates a new instance of the views you navigate to. To get around this model which I believe is wasteful and has undesired side effects, I changed the mapping for the various views to pass to one control that manages the navigation for me:

<uriMapper:UriMapper>
<uriMapper:UriMapping Uri="" MappedUri="/Views/JounceNavigation.xaml"/>
    <uriMapper:UriMapping Uri="/ShowText/{text}" MappedUri="/Views/JounceNavigation.xaml?view=TextView&text={text}"/>
    <uriMapper:UriMapping Uri="/{pageName}" MappedUri="/Views/JounceNavigation.xaml?view={pageName}"/>                        
</uriMapper:UriMapper>

Notice how I can translate the path to a view parameter, and that I am also introducing a mapping for "ShowText" that we'll use to show how you can grab parameters.

The JounceNavigation control will get a new copy every time, but by using MEF it will guarantee we always access the same container. To do this, I created NavigationContainer and it contains a single content control with a region so the region is only exported once:

<ContentControl 
                    HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                    HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
                    Regions:ExportAsRegion.RegionName="MainContainer"/>

In the code-behind, I simply export it:

namespace SilverlightNavigation.Views
{
    [Export]
    public partial class NavigationContainer
    {
        public NavigationContainer()
        {
            InitializeComponent();
        }
    }
}

Now we can use this single container in the JounceNavigation control. What we want to do is attach it when we navigate to the control, and detach it when we navigate away (a control can only have one parent, so if we don't detach it, we'll get an error when the next view is created if the previous view hasn't been garbage-collected yet).

The navigation control also does a few more things to integrate with the navigation framework. It will remember the last view so it can deactivate the view when navigating away. It will default to the "Home" view but basically takes any view passed in (based on the uri mappings we defined earlier) and raises the Jounce navigation event. Provided the view targets the region in our static container, it will appear there. Normally I'd do all of this in a view model but wanted to show it in code-behind for the sake of brevity (and to show how Jounce plays nice with standard controls as well).

public partial class JounceNavigation
{
    [Import]
    public IEventAggregator EventAggregator { get; set; }

    [Import]
    public NavigationContainer NavContainer { get; set; }

    private static string _lastView = string.Empty;
                
    public JounceNavigation()
    {
        InitializeComponent();
        CompositionInitializer.SatisfyImports(this);
        LayoutRoot.Children.Add(NavContainer);
        if (!string.IsNullOrEmpty(_lastView)) return;
        EventAggregator.Publish("Home".AsViewNavigationArgs());
        _lastView = "Home";
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        if (NavigationContext.QueryString.ContainsKey("view"))
        {
            var newView = NavigationContext.QueryString["view"];
            _lastView = newView;
            EventAggregator.Publish(_lastView.AsViewNavigationArgs()); 
            EventAggregator.Publish(NavigationContext);
        }
    }

    protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
    {
        if (!string.IsNullOrEmpty(_lastView))
        {
            EventAggregator.Publish(new ViewNavigationArgs(_lastView) {Deactivate = true});
        }
        LayoutRoot.Children.Remove(NavContainer);
    }

}

Notice that we publish two events. The first is the view navigation to wire in the target view. The second is a NavigationContext event. The navigation context contains all of the query string information. Any view that needs to pull values from the query string can simply listen for this event. Because the view navigation is called first, the view will be in focus and ready when it receives the context message to parse any parameters.

To demonstrate this, let's look at the TextView control. When you pass text in the url, it will simply display it. The XAML looks like this:

<Grid x:Name="LayoutRoot" Background="White">
    <TextBlock x:Name="TextArea"/>
</Grid>

The code-behind looks like this:

[ExportAsView("TextView")]
[ExportViewToRegion("TextView", "MainContainer")]
public partial class TextView : IEventSink<NavigationContext>, IPartImportsSatisfiedNotification
{
    [Import]
    public IEventAggregator EventAggregator { get; set; }

    public TextView()
    {
        InitializeComponent();                        
    }

    public void HandleEvent(NavigationContext publishedEvent)
    {
        if (publishedEvent.QueryString.ContainsKey("text"))
        {
            TextArea.Text = publishedEvent.QueryString["text"];
        }
    }

    public void OnImportsSatisfied()
    {
        EventAggregator.SubscribeOnDispatcher(this);
    }
}

Pretty simple - it exports as a view name, targets the main container region, and then registers as the event sink for NavigationContext messages. In this case we only have one listener. In more complex scenarios with multiple view models listening, the view model would simply inspect the "view" parameter to make sure it matches the target view (it could easily find this in a generic way by asking the view model router) and ignore the message if it does not.

To convert the "Home" and the "About" page took only two steps.

First, I changed them from Page controls to UserControl controls. I simply had to change the tag in XAML and remove the base class tag in the code-behind and the conversion was complete. Second, I tagged them as views and exported them to the main region:

namespace SilverlightNavigation.Views
{
    [ExportAsView("About")]
    [ExportViewToRegion("About", "MainContainer")]
    public partial class About 
    {
        public About()
        {
            InitializeComponent();
        }       
    }
}

That's it - now I have a fully functional Jounce application that uses the navigation framework and handles URL parameters. You can click on the "text" tab to see the sample text and then change the URL to confirm it parses the additional text you create.

Jeremy Likness