Opening up Silverlight 4 Navigation: Introduction to INavigationContentLoader

Quick links to followup posts:

If you haven’t noticed by now (or been following my previous blog posts), I happen to really enjoy exploring the Navigation feature in Silverlight.  A while back, I posted a number of workarounds and tips for some desirable scenarios using the Navigation feature in Silverlight 3.  Then… I went silent.  And the reason: I’ve been waiting for an extensibility point to be opened up in Silverlight navigation.  In the Silverlight 4 beta, that extensibility point has arrived as the INavigationContentLoader.

The ContentLoader extensibility point allows developers to handle the page loading process themselves, much as you would with a GenericHandler in ASP.NET.  You’re given some context on what to load (in this case a target Uri), then it’s up to you as the developer to decide how to load it.  Your handling of these requests can be as specialized or as generalized as you find appropriate.

On the surface, opening up this extensibility point is a rather small change, but it’s actually very powerful when used to solve general problems.  In this post, I’ll build a basic INavigationContentLoader that loads pages based on class name rather than project file structure.  In future posts, I hope to explore some more general (and possibly composable) ContentLoaders.

So… Tell me about this INavigationContentLoader interface.

Fundamentally, the INavigationContentLoader interface just provides a cancellable asynchronous pattern for loading content based upon a target Uri.  You can set a ContentLoader on the Frame control, which will cause the NavigationService to use your ContentLoader rather than built-in one, which is publicly available as the PageResourceContentLoader.

Here’s what the interface looks like:

public interface INavigationContentLoader
{
    IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState);
    void CancelLoad(IAsyncResult asyncResult);
    bool CanLoad(Uri targetUri, Uri currentUri);
    LoadResult EndLoad(IAsyncResult asyncResult);
}

The basic flow of its use with the Frame control and the NavigationService in the Silverlight SDK is as follows:

  • Frame/NavigationService receives a request to navigate to a Uri
  • Frame/NavigationService uses its UriMapper to map the Uri it was given to a Uri for use by the content loader
  • Frame/NavigationService passes the current (post-mapping) Uri to the content loader as well as the target (post-mapping) Uri to the content loader, calling BeginLoad
  • Frame/NavigationService waits for a callback from the content loader.  If StopLoading() was called, the Frame/NavigationService calls CancelLoad
  • Upon being called back, the Frame/NavigationService calls EndLoad() on the content loader, receiving a LoadResult
    • If the LoadResult contains a redirect Uri, the Frame/NavigationService begins a new load with that Uri as the target – without adding a history entry for the target Uri
    • Otherwise, the Frame/NavigationService attempts to display the fully-initialized UserControl or Page in the LoadResult

Great, I guess…  Can you show me how to write one?

Sure!  First, let’s choose a type of ContentLoader we’d like to create.  For this ContentLoader, we’ll begin to break free of the file structure-driven bonds of the existing ContentLoader provided in Silverlight 3.  Instead, we’ll allow navigation to Pages based upon the type name of the Page.  For example, if I have a page called “MyNamespace.MyPage”, I should be able to navigate a frame to the relative Uri “MyNamespace.MyPage” or “/MyNamespace.MyPage” rather than having to point to the XAML file for the Page.

There are two parts to writing a ContentLoader: implementing the interface and implementing IAsyncResult to store the results of your asynchronous operation.  There is ample documentation for writing an IAsyncResult out there, so I’ll conveniently ignore that for now :).

So, here’s our plan:

  • Implement INavigationContentLoader
    • Write a helper function that extracts a type name from a Uri
    • Implement CanLoad to check to see if the type specified by the targetUri can be found and has a default constructor
    • Implement BeginLoad to produce an instance of the page specified by the type in targetUri, store it in an IAsyncResult, and call the consumer’s callback
    • Implement EndLoad to wrap the page in a LoadResult and return it to the consumer
  • Implement IAsyncResult
    • Add a “Result” property to hold the constructed page
  • Use the ContentLoader
    • Specify the ContentLoader in XAML
    • Update Hyperlinks to navigate using our new Uri scheme

Alright, let’s hop to it!

Implementing INavigationContentLoader

Let’s start by implementing the interface:

public class TypenameContentLoader : INavigationContentLoader
{
}

Ok – easy!  Granted, it’s a little empty, but it’s a start!  Now, let’s flesh it out!

To make sense of our Uri scheme, we’ll write a method to extract a Type name from a relative Uri:

private string GetTypeNameFromUri(Uri uri)
{
    if (!uri.IsAbsoluteUri)
        uri = new Uri(new Uri("dummy:///", UriKind.Absolute), uri.OriginalString);
    return Uri.UnescapeDataString(uri.AbsolutePath.Substring(1));
}

This method is pretty straightforward.  First, it turns the relative Uri into an absolute one, using a dummy protocol.  Then, it uses the Path of that Uri as the Type name (minus a leading slash and after unescaping any Uri-encoded values (e.g. %20 becomes a space character, so assembly-qualified type names can be used).  The reason we don’t just use the OriginalString on the uri is because the Uri may contain a query string or a fragment (e.g. “TypeName?query=string#fragment”) – in this case we only want the Path in the Uri.

Next, we’ll implement CanLoad, which is called by the Frame/NavigationService before attempting to load.  Here, we’ll check to see that the type exists and has a default constructor – both of which we’ll need in order to create an instance of the page:

public bool CanLoad(Uri targetUri, Uri currentUri)
{
    string typeName = GetTypeNameFromUri(targetUri);
    Type t = Type.GetType(typeName, false, true);
    if (t == null)
        return false;
    var defaultConstructor = t.GetConstructor(new Type[0]);
    if (defaultConstructor == null)
        return false;
    return true;
}


By implementing CanLoad, we can now safely assume (barring any concurrency-related race condition that could arise, but doesn’t apply for the ContentLoader we’re writing in this post because it operates neither statefully nor asynchronously) that the Frame/ContentLoader will only call BeginLoad after making this check.  As such, our BeginLoad can just go ahead and try to create an instance of the type specified in the targetUri.  Then, it must store the instance in an IAsyncResult, invoke the userCallback, and wait for EndLoad to be called:

public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState)
{
    var result = new TypenameContentLoaderAsyncResult(asyncState);
    Type t = Type.GetType(GetTypeNameFromUri(targetUri), false, true);
    object instance = Activator.CreateInstance(t);
    result.Result = instance;
    userCallback(result);
    return result;
}


Our implementation of EndLoad will extract the instance of the page from the IAsyncResult and wrap it in a LoadResult.  LoadResult is a simple class we introduced to represent the result of a load operation.  It currently allows for two possible outcomes from a ContentLoader: (1) return/display a Page or UserControl, (2) provide a Uri to which users should be redirected (no history entry for the original page will be created).

public LoadResult EndLoad(IAsyncResult asyncResult)
{
    return new LoadResult(((TypenameContentLoaderAsyncResult)asyncResult).Result);
}


The final method of the INavigationContentLoader interface is CancelLoad.  In our case, creating an instance of the page happens synchronously, so cancellation doesn’t really have any meaning or value.  Other ContentLoaders, however, might use this method to stop a download or abort a large operation:

public void CancelLoad(IAsyncResult asyncResult)
{
    return;
}


And that’s it!  Our Typename-based content loader is complete!  A simple implementation of IAsyncResult will allow us to compile:

internal class TypenameContentLoaderAsyncResult: IAsyncResult
{
    public object Result { get; set; }

    // Other IAsyncResult members
}

Consuming the ContentLoader

Consuming our new ContentLoader is as simple as setting the ContentLoader property on your Frame in XAML:

<navigation:Frame xmlns:loader="clr-namespace:TypenameContentLoader.ContentLoader"
                  x:Name="ContentFrame"
                  Source="/TypenameContentLoader.Views.Home">
                  <navigation:Frame.ContentLoader>
                      <loader:TypenameContentLoader />
                  </navigation:Frame.ContentLoader>
</navigation:Frame>

You’ll note that in addition to setting the ContentLoader, I’ve set the source for the Frame control to match our new Uri scheme.  We must also do the same for other hyperlinks in the application:

<HyperlinkButton x:Name="Link1" Style="{StaticResource LinkStyle}"
                 NavigateUri="/TypenameContentLoader.Views.Home" TargetName="ContentFrame" Content="home"/>

<HyperlinkButton x:Name="Link2" Style="{StaticResource LinkStyle}"
                 NavigateUri="/TypenameContentLoader.Views.About, TypenameContentLoader, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" TargetName="ContentFrame" Content="about"/>

<HyperlinkButton x:Name="Link3" Style="{StaticResource LinkStyle}"
                 NavigateUri="/TypenameContentLoader.Views.ParameterizedPage?a=100&amp;b=1000"
                 TargetName="ContentFrame" Content="parameterized?a=100&amp;b=1000"/>

And there you have it!  The application is fully configured to use the new ContentLoader.  You’ll notice that the Frame will happily pass along the complex Uris in all three of the hyperlinks above, and the loader will load them correctly.  Query string values still get parsed by the navigation service, and all is well with the world :)

Ok, so now I can build a ContentLoader – can I actually see the code?

Yep, and I’ll do you one better!  You can also try the live sample!

And, just to prove that query strings “just work”, try this link into the application with a long list of key/value pairs that get used by the page:

A long query string with the new content loader

Feel free to play around with it and let me know what you think!

(Also note the screenshot above using Google Chrome – now a supported browser with Silverlight 4!)

Ok, so all of this is cool, but why is it useful?  What’s next?

This Typename-based content loader is a neat trick to try, but its usefulness is still pretty limited.  The extensibility point itself, however, is very powerful.  Instead of having to come up with hacks and workarounds for things like on-demand downloading of assemblies containing pages as I did before, we can bake that logic right into a content loader!  If you are an adherent of the MVVM pattern, you can use your content loader to connect your view to your model.  If all you need to do is specify constructor parameters to your pages, you now have that option!  If you need to pre-load data from a web service before navigating to a page, a content loader can help!

The ContentLoader extensibility point introduced in the Silverlight 4 beta really opens up Silverlight Navigation, allowing you to interject in the page loading process and enabling you to make navigation work with whatever framework or application structure you choose.

I’ve got a bunch of ideas for useful ContentLoaders that I’ll explore (and hopefully blog about) going forward.  If you’ve got ideas, questions, or feedback, please let me know!  I look forward to hearing from you!

21 thoughts on “Opening up Silverlight 4 Navigation: Introduction to INavigationContentLoader”

    1. Indeed, I think both MEF and PRISM might have some good prospects here :). I’ve been chatting with Glenn Block and David Hill about how these things might work together.

  1. Hi David,

    your article is very helpful. Now i have one question:
    Is it possible to pass objects from one page to another page via the ContentLoader? If yes, how i can do this.

  2. Is there a way to get a reference to the navigation service other than waiting for a page to load and grabbing the reference from the page property? I have a custom Navigation Content Provider and need access to the navigation service across the application.

    Thanks

  3. I need to open navigation pages inside TabControl.
    For this I implemented INavigationContentLoader based on this code using dummy Page() control. Navigation is invoked using

    public static void Navigate(string form)
    {
    App.Current.RootVisual.ContentFrame.Navigate(new Uri(form, UriKind.Relative));
    }

    If user clicks rapidly browser back and forward buttons, browser history menu contains duplicate names and name does not correspond to page.
    How to fix this ?

    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Navigation;
    using MyApp.UI;

    namespace TypenameContentLoader.ContentLoader
    {
    public class TypenameContentLoader : INavigationContentLoader
    {
    string GetTypeNameFromUri(Uri uri)
    {
    if (!uri.IsAbsoluteUri)
    uri = new Uri(new Uri(“dummy:///”, UriKind.Absolute), uri.OriginalString);
    return Uri.UnescapeDataString(uri.AbsolutePath.Substring(1));
    }

    #region INavigationContentLoader Members

    public bool CanLoad(Uri targetUri, Uri currentUri)
    {
    return true;
    }

    public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState)
    {
    var result = new TypenameContentLoaderAsyncResult(asyncState);
    var res = GetTypeNameFromUri(targetUri);
    var page = new Page();
    if (res != “”)
    {
    var frm = FormManager.LoadForm(res);
    page.Title = FormManager.Header(res);
    }
    result.Result = page;
    userCallback(result);
    return result;
    }

    public LoadResult EndLoad(IAsyncResult asyncResult)
    {
    return new LoadResult(((TypenameContentLoaderAsyncResult)asyncResult).Result);
    }

    public void CancelLoad(IAsyncResult asyncResult)
    {
    return;
    }

    #endregion
    }
    }

    This is also discussed in http://forums.silverlight.net/forums/p/187817/431200.aspx

  4. Hi,

    We are planning to develop a community site using silverlight4. We have two issues one related to SL navigation and another general concept. We need ur suggestion.

    Issue 1#: We need to generate the menu and submenu at run time depending on user role. User role name and pages(URL) they can authorize to access are being defined in database( we have separate interface for that). On login we need to read the database and generate menu/submenu according to role of the user. What is best option to do this in SL.

    Issue 2#: Second issue is general architectural concept. What is best way to store article submitted by user while developing a community site.

    Please share your knowledge and useful link to solve our problem and help us to develop a robust Sl application):

    Thanks
    Sagar

    1. Sagar,

      The best suggestion I can give you is to look at my sample here: http://www.davidpoll.com/2010/05/10/common-navigation-ui-and-authorization-driven-sitemaps/

      The sitemap UI I present is TreeView based and uses a declarative sitemap, limiting visibility of nodes in the sitemap based upon authorization. It’s all extensible and doesn’t have to be defined in XAML — this is just the experience I chose for the 90% case. If you run into something that’s broken with it, please do let me know :)

      As for your second issue, it really depends on what their content is. If users are using the RichTextBox control to generate content, for example, you might store the XAML they generate directly into a database (just as a string) and use XamlReader.Load() on the content to pull it back out. I actually have a richer example of this here: http://www.davidpoll.com/2010/07/25/to-xaml-with-love-an-experiment-with-xaml-serialization-in-silverlight/

      If the RichTextBox only has plain inlines (e.g. Hyperlinks, Runs, Bolds, Italics, etc.) and nothing richer (e.g. Images, inline UI, etc.), then you can just use the .Xaml property of the RichTextBox to get the XAML back out. Otherwise, the UiXamlSerializer in my post above may be able to help.

      I hope that helps!
      -David

  5. Hey David, thanks for the good work.

    So here’s the thing: what if the page is in another assembly, how can it be viewed in the current assembly.

  6. hmm…trying to combine this with the mef loader code.
    I was hoping that after this code:
    SLaB.Utilities.Xap.Xap resultingXap =
    this._XapLoader.EndLoadXap(res);
    this._Loader._FoundXaps[authority] =
    resultingXap;
    this.FinishLoading(
    this._Loader._FoundXaps[authority],
    helper,
    currentUri);
    …one would just call
    CompositionInitializer.SatisfyImports(this);
    and the exported parts rom the loaded xap file would appear in the list.
    well…they don’t.
    is there any way to make this work?(i don’t want to use the MEF Loader…i want the xap loader logic followed by the import. :)

Leave a Reply