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

12 comments:

  1. Great post, I will download your sample and give it a try.

    ReplyDelete
  2. I started the frame work to try it out. I created a simple view with label which is bound to a field called Name which is defined in the view model. I decorated view and viewmodel with ExportAsView and ExportAsViewModel. I also did ViewModelRoute to add view and viewmodel with the same name in the decoration in the view. I was expecting that when I run this program, binding should happen and default value set in the Name field in the viewmodel to show up but it does not show up. what do you think am I missing? By the I have added application life time object as well. I was able to see the 'welcome to jounce' before I added my label field. I am trying to understand the view and viewmodel binding.

    When I debug the code, I am able to see the code does set into my getter method of the Name field.
    Thanks,

    ReplyDelete
  3. Did you add "IsShell=true" to the view?

    ReplyDelete
  4. Here is the code
    [ExportAsView("Shell", IsShell=true)]
    public partial class MainPage : UserControl
    {
    public MainPage()
    {
    InitializeComponent();
    }

    [Export]
    public ViewModelRoute Binding
    {
    get
    {
    ViewModelRoute route = ViewModelRoute.Create("Shell", "Shell");
    return route;
    }
    }
    }

    View model
    [ExportAsViewModel("Shell")]
    public class MainPageViewModel:BaseViewModel
    {
    private string _name;

    public string Name
    {
    get
    {
    if (_name == null)
    {
    _name = "dbcuser";
    }
    return _name;
    }
    set
    {
    _name = value;
    RaisePropertyChanged(() => Name);
    }
    }
    }

    ReplyDelete
  5. :) sorry found my problem, the code I copied had a XAML bug, instead of using Text="{Binding Name}" it had DataContext="{Binding Name}" it works perfectly.
    Thanks,

    ReplyDelete
  6. No worries, appreciate you sharing this with us.

    ReplyDelete
  7. Hi Jeremy, Thank you very much for all your hard work -- I like Jounce a lot and would like to incorporate it in my current Silverlight projects. I've modified your Silverlight Navigation quick reference by adding additional navigation link to the view hosted in the external .xap file:



    Here's external view definition:

    [ExportAsView("ExternalModuleMainPage")] [ExportViewToRegion("ExternalModuleMainPage", "MainContainer")]
    public partial class MainPage
    {
    public MainPage()
    {
    InitializeComponent();
    }
    }

    In order to let Jounce know where to find this view, I've exported XapViewRoute to link this view to the external .Xap file:

    public class ExternalModuleRoutes
    {
    [Export]
    public ViewXapRoute ExternalModuleMainPageRoute
    {
    get { return ViewXapRoute.Create("ExternalModuleMainPage", "ExternalModule.xap");
    }
    }
    }

    Now, when I run the project, navigation works fine to the Home, About, and Text views but fails to navigate to the externally hosted view... The previous page is still shown and no exceptions are thrown. As far as I can see during the debugging session, the callback lambda never get called in the following code fragment from ViewRouter.HandleEvent method, and therefore view is not activated:

    if (viewLocation != null)
    { DeploymentService.RequestXap(viewLocation.ViewXap, exception => { if (exception != null) { throw exception; } _ActivateView(e.ViewType); });
    }
    else
    {
    // just activate the view directly
    _ActivateView(e.ViewType);
    }

    Also, ReagionManager.HandleEvent handler never got called for the external view type too.

    Would you please look into this scenario: either I'm doing something wrong or there's a problem in the framework code.

    I'd appreciate your help,
    Eugene.

    ReplyDelete
  8. I used this scenario all of the time so I'm assuming it's something with your configuration. The most common mistake is to use the auto-generated HTML test page as your starting URL. This causes a file:/// url to be fed to the deployment catalog, which fails. Make sure you are launching from an actual web site, either through the built-in developer one by using an .ASPX page instead of an .HTML page as the start page, or by publishing it.

    Have you opened up Fiddler to see if the request is going out for the XAP?

    ReplyDelete
  9. Thank you for your quick response, Jeremy. I do believe I'm correctly starting my SL applications having aspx page as the start up page. Here's what I see when running Fiddler: 200 result when accessing site for the first time:

    GET http://evinnik/TestRunJounce.Web/TestRunJounceTestPage.aspx HTTP/1.1
    Accept: image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/vnd.ms-xpsdocument, application/xaml+xml, application/x-ms-xbap, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-shockwave-flash, */*
    Referer: http://evinnik/TestRunJounce.Web/
    Accept-Language: en-us
    User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; Trident/4.0; GTB6.6; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.5.21022; .NET CLR 3.5.30729; InfoPath.2; OfficeLiveConnector.1.3; OfficeLivePatch.0.0; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E) chromeframe/7.0.517.44
    Accept-Encoding: gzip, deflate
    Host: evinnik
    Connection: Keep-Alive

    Than there's 304 result showing that "main" Xap file has been loaded:


    GET http://evinnik/TestRunJounce.Web/ClientBin/TestRunJounce.xap HTTP/1.1
    Accept: */*
    Accept-Encoding: gzip, deflate
    User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; Trident/4.0; GTB6.6; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.5.21022; .NET CLR 3.5.30729; InfoPath.2; OfficeLiveConnector.1.3; OfficeLivePatch.0.0; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)
    If-Modified-Since: Wed, 17 Nov 2010 04:00:38 GMT
    If-None-Match: "5c34c8feb86cb1:0"
    Host: evinnik
    Connection: Keep-Alive

    And, finally, when I click on the External Module link, Fiddler shows 304 result for ExternalModule.xap being downloaded:

    GET http://evinnik/TestRunJounce.Web/ClientBin/externalmodule.xap HTTP/1.1
    Accept: */*
    Referer: http://evinnik/TestRunJounce.Web/ClientBin/TestRunJounce.xap
    Accept-Language: en-us
    Accept-Encoding: gzip, deflate
    User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; Trident/4.0; GTB6.6; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.5.21022; .NET CLR 3.5.30729; InfoPath.2; OfficeLiveConnector.1.3; OfficeLivePatch.0.0; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)
    If-Modified-Since: Wed, 17 Nov 2010 04:00:36 GMT
    If-None-Match: "45419dfdb86cb1:0"
    Host: evinnik
    Connection: Keep-Alive

    Nevertheless, ExternalModuleMainPage is not rendered -- instead, previous page has been shown though IE address bar shows the correct URI:

    http://evinnik/TestRunJounce.Web/TestRunJounceTestPage.aspx#/ExternalModuleMainPage

    If you still think that something is wrong with the way my solution is configured, would you please be able to post your code that implements similar scenario (SL navigation with dynamically loaded modules) that works so that I'll be able to find the problem?

    As always, thank you very much for what you've been doing for the Silverlight Community!

    Eugene.

    ReplyDelete
  10. Hi Jeremy, on one of the forums (http://www.wintellect.com/CS/blogs/jlikness/archive/2009/09/29/decoupled-childwindow-dialogs-with-prism-in-silverlight-3.aspx) you have demonstrated how child windows can be decoupled using prism. Can you please let me know how do we implement a simple dialog service using Jounce (event aggregator)?

    ReplyDelete
  11. Sure - this is the process I use, but I will update the documentation for Jounce as well:

    Simple Dialog Service

    ReplyDelete