A while back, I blogged about the INavigationContentLoader interface introduced in Silverlight 4. INavigationContentLoader is an extensibility point in Silverlight’s navigation framework that lets you provide your own plug-in for loading pages. Silverlight 4 comes with one INavigationContentLoader implementation in a class named PageResourceContentLoader, which loads pages from assemblies in an application’s XAP file. I recently put INavigationContentLoader to work by building my own content loader that loads pages from local XAP files as well as remote assemblies. I named my implementation DynamicContentLoader, and you can download a Visual Studio 2010 project that uses it from Wintellect’s Web site.

My goal in building DynamicContentLoader was to create a content loader that supports the partitioning of large navigation apps. Imagine you’re writing a navigation app that contains hundreds, perhaps thousands, of pages. Depending on how the user interacts with the app, you may not need all those pages, and you don’t want every user to have to pay the price for downloading them, which is exactly what happens if you put all those pages in the application’s XAP. DynamicContentLoader supports a special syntax that lets you identify auxiliary assemblies containing “external” pages. The first time you load an external page, DynamicContentLoader downloads the assembly, loads it into the appdomain, and creates the page. It also caches information allowing that page (and other pages in the same assembly) to be loaded again without redownloading the assembly.

My sample begins with the following goo in MainPage.xaml:

<Grid x:Name="LayoutRoot" Background="White">

  <nav:Frame Source="Page1">

    <nav:Frame.ContentLoader>

      <local:DynamicContentLoader />

    </nav:Frame.ContentLoader>

    <nav:Frame.UriMapper>

      <map:UriMapper>

        <map:UriMapping Uri="" MappedUri="/Page1.xaml" />

        <map:UriMapping Uri="Page1" MappedUri="/Page1.xaml" />

        <map:UriMapping Uri="Page2" MappedUri="/EXT:ExternalPages.dll|Page2.xaml" />

        <map:UriMapping Uri="Page3" MappedUri="/EXT:ExternalPages.dll|Page3.xaml" />

      </map:UriMapper>

    </nav:Frame.UriMapper>

  </nav:Frame>

</Grid>

The URI mappings target the app’s three pages: Page1.xaml, Page2.xaml, and Page3.xaml. Page1.xaml lives in the application’s XAP file. Page2.xaml and Page3.xaml do not; they live in an external assembly named ExternalPages.dll. That assembly isn’t embedded in the XAP; it was created from a separate Silverlight project and copied into ClientBin, where it sits beside the application’s XAP file. If the user never navigates to Page2.xaml or Page3.xaml, the assembly never gets loaded. But the moment the user navigates to one of these pages, ExternalPages.dll gets downloaded from ClientBin and loaded into the application. There is no limit to the number of auxiliary assemblies you can deploy. If you wanted to add pages 4 and 5 to the app and house them in an assembly named MorePages.dll, you could modify the URI mappings as follows:

<map:UriMapping Uri="" MappedUri="/Page1.xaml" />

<map:UriMapping Uri="Page1" MappedUri="/Page1.xaml" />

<map:UriMapping Uri="Page2" MappedUri="/EXT:ExternalPages.dll|Page2.xaml" />

<map:UriMapping Uri="Page3" MappedUri="/EXT:ExternalPages.dll|Page3.xaml" />

<map:UriMapping Uri="Page4" MappedUri="/EXT:MorePages.dll|Page4.xaml" />

<map:UriMapping Uri="Page5" MappedUri="/EXT:MorePages.dll|Page5.xaml" />

All you have to do is preface the assembly URI with /EXT:, and separate the assembly URI from the page URI with a vertical bar (|). DynamicContentLoader will do the rest.

My DynamicContentLoader class is implemented as follows:

public class DynamicContentLoader : INavigationContentLoader

{

    private const string _extern = "/EXT:";

    private const char _separator = ‘|’;

    private PageResourceContentLoader _loader = new PageResourceContentLoader();

 

    // Maps URIs to types (e.g., "/EXT:ExternalPages.dll/Page2.xaml" -> typeof(Page2))

    // Used to determine whether a page has been requested before and to instantiate it

    // quickly if it has

    private static Dictionary<string, Type> _pages = new Dictionary<string, Type>();

 

    // Maps downloaded DLLs to assembly info (e.g., "ExternalPages.dll" ->

    // "ExternalPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")

    // Used to determine whether an assembly has been downloaded before and to

    // store info needed to get information about types in that assembly

    private static Dictionary<string, string> _assemblies = new Dictionary<string, string>();

 

    public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri,

        AsyncCallback userCallback, object asyncState)

    {

        if (!targetUri.ToString().StartsWith(_extern))

        {

            // If the URI doesn’t start with "EXT:," let the default loader handle it

            return _loader.BeginLoad(targetUri, currentUri, userCallback, asyncState);

        }

        else // Otherwise handle it here

        {

            NavigationAsyncResult ar = new NavigationAsyncResult(userCallback, asyncState);

            string fullUri = targetUri.ToString();

 

            // If this page has been loaded before, instantiate it

            // using type information generated the first time and

            // cached in the _pages dictionary

            Type type;

 

            if (_pages.TryGetValue(fullUri.ToLower(), out type))

            {

                ar.Result = Activator.CreateInstance(type);

                ar.CompleteCall(true);

                return ar;

            }

 

            // Extract the page URI (e.g., "Page2.xaml")

            int index = fullUri.IndexOf(_separator) + 1;

            string pageUri = fullUri.Substring(index, fullUri.Length – index);

 

            if ((index = pageUri.IndexOf(‘?’)) > 0)

                pageUri = pageUri.Substring(0, index); // Strip off query string

 

            // Extract the assembly URI (e.g., "ExternalPages.dll")

            int len = _extern.Length;

            string assemblyUri = fullUri.Substring(len, fullUri.Length – len);

            assemblyUri = assemblyUri.Substring(0, assemblyUri.IndexOf(_separator));

 

            // If the assembly has been downloaded before, instantiate

            // the page without downloading the assembly again

            string fullName;

 

            if (_assemblies.TryGetValue(assemblyUri.ToLower(), out fullName))

            {

                // Instantiate the page

                type = GetXamlPageType(pageUri, fullName);

                ar.Result = Activator.CreateInstance(type);

                _pages.Add(fullUri.ToLower(), type);

                ar.CompleteCall(true);

                return ar;

            }

 

            // Prepare the IAsyncResult

            ar.TargetUri = fullUri;

            ar.PageUri = pageUri;

            ar.AssemblyUri = assemblyUri;

 

            // Begin downloading the assembly

            WebClient wc = new WebClient();

            wc.OpenReadCompleted += new OpenReadCompletedEventHandler(OnOpenReadCompleted);

            wc.OpenReadAsync(new Uri(assemblyUri, UriKind.Relative), ar);

            return ar;

        }

    }

 

    void OnOpenReadCompleted(object sender, OpenReadCompletedEventArgs e)

    {

        if (e.Error == null)

        {

            NavigationAsyncResult ar = e.UserState as NavigationAsyncResult;

 

            // Load the downloaded assembly into the appdomain

            AssemblyPart part = new AssemblyPart();

            Assembly assembly = part.Load(e.Result);

            string fullName = assembly.FullName;

 

            // Instantiate the page

            Type type = GetXamlPageType(ar.PageUri, fullName);

            ar.Result = Activator.CreateInstance(type);

 

            // Update the dictionaries

            _pages.Add(ar.TargetUri.ToLower(), type);

            _assemblies.Add(ar.AssemblyUri.ToLower(), fullName);

 

            // Signal that loading is finished

            ar.CompleteCall(false);

        }

        else

            throw e.Error;

    }

 

    public bool CanLoad(Uri targetUri, Uri currentUri)

    {

        if (targetUri.ToString().StartsWith(_extern))

            return true;

        else

            return _loader.CanLoad(targetUri, currentUri);

    }

 

    public void CancelLoad(IAsyncResult asyncResult)

    {

        // Do nothing

    }

 

    public LoadResult EndLoad(IAsyncResult asyncResult)

    {

        if (asyncResult is NavigationAsyncResult)

            return new LoadResult((asyncResult as NavigationAsyncResult).Result);

        else

            return _loader.EndLoad(asyncResult);

    }

 

    /////////////////////////////////////////////////////////////////

    // Given the URI of a XAML page (e.g., Page2.xaml) and the name

    // of the assembly that hosts the page, GetXamlPageType extracts

    // the page from the assembly, finds the x:Class attribute, and

    // returns the corresponding type.

    //

 

    private Type GetXamlPageType(string pageUri, string assemblyFullName)

    {

        string shortName = assemblyFullName.Substring(0, assemblyFullName.IndexOf(‘,’));

        string path = shortName + ";component/" + pageUri;

        StreamResourceInfo sri = Application.GetResourceStream(new Uri(path,

            UriKind.Relative));

 

        using (XmlReader reader = XmlReader.Create(sri.Stream))

        {

            reader.Read();

            string name = reader.GetAttribute("x:Class");

            return Type.GetType(name + ", " + assemblyFullName, false, true);

        }

    }

}

There’s a lot going on inside, but the gist of it is that before loading a new page, the navigation framework calls the registered content loader’s CanLoad method to determine whether the page can be loaded. DynamicContentLoader’s CanLoad method checks for a /EXT: prefix at the beginning of the URI. If the prefix is present, CanLoad returns true. If there is no such prefix, CanLoad delegates to an instance of PageResourceContentLoader so that “normal” pages will work as usual.

The real action happens in BeginLoad, which is called to begin an asynchronous page load. The workflow in my implementation can be summed up as follows:

  1. Check to see if the requested URI contains a /EXT: prefix. If not, delegate to PageResourceContentLoader to do the loading.
  2. Check the _pages dictionary to see if the page (and the assembly containing it) has been loaded before. If so, extract a Type object representing the page from the dictionary and pass the Type to Activator.CreateInstance to create an instance of that page.
  3. If the page has not been loaded before, check the _assemblies dictionary to see whether the assembly designated in the URI has been loaded before. If so, inspect the assembly and create a Type object representing the page. Then use Activator.CreateInstance to create an instance of the page and cache the Type object in the _pages dictionary.
  4. If neither the page nor the assembly has been loaded before, use WebClient to asynchronously download the assembly. When the download is complete, load the assembly into the appdomain with AssemblyPart.Load and create a Type object representing the page. Then use Activator.CreateInstance to create an instance of the page, and cache the Type object in the _pages dictionary and information about the downloaded assembly in the _assemblies dictionary.

The helper method named GetXamlPageType plays a key role in content loading. Given the URI of a page resource (for example, Page2.xaml), it uses a little trick with Application.GetResourceInfo to extract the resource from the designated assembly. Then it uses an XmlReader to find the x:Class attribute on the page’s root element. That attribute identifies the class that corresponds to the XAML page.

David Poll has blogged extensively about INavigationContentLoader, but to my knowledge, this is the only example out there of a content loader that loads pages dynamically based on XAML page URIs rather than class names. One idea I have for extending DynamicContentLoader is to give it the ability to download auxiliary XAPs containing external pages. Conceptually, it wouldn’t be hard: just download the XAP file, enumerate the assemblies inside, and load them one by one with AssemblyPart.Load. I’ve done that in other extensibility demos, and it’s not difficult. But because I just landed in Beijing in preparation for spending a week at the Microsoft office here, and because I want to do some sightseeing before work starts tomorrow, for now, I’ll leave that enhancement up to you. 🙂

UPDATE: I modified the code to strip query strings from page URIs. The downloadable zip file containing the finished project has been updated accordingly.