Way back in July, shortly after Silverlight 3 was released, I posted a technique that allowed you to use the Navigation framework in the SDK to load pages in dlls that would be downloaded as part of the navigation process.  The solution relied on two things: a workaround to the navigation framework’s inability to navigate to Pages in dynamically-loaded assemblies, and a derived version of the Frame class that hid many methods in order to orchestrate downloads of dlls and their dependencies.

With Silverlight 4’s INavigationContentLoader extensibility point, we can address this scenario much more effectively, and are no longer locked into the workarounds and strict constraints that Silverlight 3’s navigation feature placed on us.  In this post, I’ll walk through the use of another ContentLoader I’ve been working on and look at how it simplifies building multi-xap applications.

Getting a jump-start

If you’d like to get started quickly and see the magic of multi-xap applications, follow these steps (note: Requires the Silverlight 4 Beta and an active internet connection) or download the source for the steps below here (you may need to fix up the references after downloading SLaB):

  1. Download and extract SLaB
  2. Open Visual Studio 2010 and create a new Silverlight 4 project using the Silverlight Navigation Application template.  When prompted to create a corresponding ASP.NET web application, just click “OK” and let Visual Studio create a web project for you.
  3. Add a reference to SLaB.Navigation.ContentLoaders.Xap.dll from the extracted Binaries folder.
  4. Add the following line of code to App.xaml.cs: PackUriParser.Initialize();
  5. Replace the Frame in MainPage.xaml with the following code (new code is bold/italic– the rest is identical to the default project xaml except for formatting):
<navigation:Frame x:Name="ContentFrame"
                  Style="{StaticResource ContentFrameStyle}"
                  Source="/Home"
                  Navigated="ContentFrame_Navigated"
                  NavigationFailed="ContentFrame_NavigationFailed">
    <navigation:Frame.UriMapper>
        <uriMapper:UriMapper>
            <uriMapper:UriMapping Uri=""
                                  MappedUri="/Views/Home.xaml" />
            <uriMapper:UriMapping Uri="/{pageName}"
                                  MappedUri="/Views/{pageName}.xaml" />
        </uriMapper:UriMapper>
    </navigation:Frame.UriMapper>
    <navigation:Frame.ContentLoader>
        <SLaB:XapContentLoader xmlns:SLaB="clr-namespace:SLaB.Navigation.ContentLoaders.Xap;assembly=SLaB.Navigation.ContentLoaders.Xap"
                               EnableCrossDomain="True" />
    </navigation:Frame.ContentLoader>
</navigation:Frame>

  1. Run the application.
  2. Replace “#/Home” in your browser’s address bar with “#pack://http:,,open.depoll.com,SimpleApplication,SimpleApplication.xap/SimpleApplication;component/Depoll.xaml?Source=http://open.depoll.com&File=wildlife.wmv”
  3. Wait for a moment, and then enjoy the (admittedly underwhelming) show!

Whoa, what just happened?!

You just created an application that loaded a page in another xap!  It took only two actions to make this happen: registering the “PackUriParser” and setting the Frame’s ContentLoader to a XapContentLoader with cross-domain access enabled.  When you added the “pack://…” to the browser’s address bar, you told the content loader to downlaod a xap at http://open.depoll.com/SimpleApplication/SimpleApplication.xap (which is on my personal site, and has a very permissive cross-domain policy) and load the page “Depoll.xaml”.

The XapContentLoader makes loading pages in other xaps (and reducing your download sizes) as easy as coming up with a URI that points to the page.  Read on to learn more about how to use the XapContentLoader and some of the problems associated with multi-xap applications.

Why load pages in external xaps?

This has always been a particularly interesting scenario to me.  The size of your application’s xap file can have a big impact on your users’ experience with your Silverlight application.  Since the entire xap and its dependencies (e.g. cached assemblies) must download before users can interact with your application, keeping the xap’s size small means users can start using your application sooner and are less likely to give up and click away.

Partitioning your application into smaller-sized chunks that download only when users attempt to access that piece of functionality can also save bandwidth (since users need not download parts of the application they won’t use).

The folks behind both MEF (Package catalogs) and Prism (Modules) have great libraries that help you accomplish this partitioning.  To me, navigation is a natural delineator of application functionality – Pages represent pieces of functionality your users can access, so triggering loading of other assemblies/xaps as part of navigation means that users will get the pieces of the application when they want to use them.

In this post, I will be outlining a XapContentLoader, which makes loading pages in other Xaps a 2-lines-of-code problem to solve.  I’ve added this ContentLoader and some related utilities to SLaB, so you can use it yourself or play with the code if you like!

XapLoader

The first component I needed to make this scenario work was a way to download and load xap files.  As such, I wrote a XapLoader utility.  The strategy I used for loading xaps was the following:

  1. Download the xap
  2. Read the AppManifest.xaml within the xap to locate the assemblies therein
  3. Load all assemblies within the xap
  4. Download zip files for the “ExtensionParts” within the AppManifest (used for the “cached assemblies” feature)
  5. Load the assembly associated with each zip file

Why worry about ExtensionParts?  My hope is to use this feature to solve the shared dependency problem.  For example, the System.Windows.Controls.Data.dll file in the SDK (containing the DataGrid control) is a fairly large assembly (446 kb uncompressed).  I’d like to avoid having users download this assembly when the app first loads, so I partition my application into four xaps: Main.xap, Foo.xap, Bar.xap, and Baz.xap.  Foo.xap and Bar.xap both use the DataGrid control.  Since I have no guarantees about the order in which my users will access the application, I need to make sure both of those xaps have access to that assembly, so I partition my application as follows:

Multiple xaps have a shared dependencyThis is a bit painful, since now when I download both Foo.xap and Bar.xap, I’m forced to download System.Windows.Controls.Data.dll twice!  Instead, if I turn on the assembly caching feature introduced in Silverlight 3, I can get both Foo.xap and Bar.xap to point to the same System.Windows.Controls.Data.dll.  By taking it out of the actual xaps and making it a separate download, I need only download it once (in fact, the browser’s cache will take care of it for me!).  Now, my application is partitioned in this way:

Xaps with a cached assembly dependency

And now I’ve effectively partitioned the application without keeping redundant libraries around that would increase the overall download size of the application.

My implementation allows you to do this (using the same mechanism as “assembly caching”), but has the following limitations:

  • Cached assembly .zip files must contain only one assembly per zip.
  • The name of the .zip file must be the same as the assembly name (replacing “.dll” with “.zip”)

This loader returns a “Xap” object, which has all of the loaded assemblies related with the xap, a “Manifest”, which contains the same data as the Deployment defined in AppManifest.xaml within the xap, and a means to get the original streams from which each assembly was loaded (for use in keeping local copies, if that’s what you’d like to do).  The XapContentLoader, described below, uses this loader to find and load xaps that would contain additional pages.

XapContentLoader

The XapContentLoader is an implementation of INavigationContentLoader that will download an external xap and load a page in one of its assemblies.  This content loader uses a slightly modified version of the pack uri scheme (which I’m probably abusing a bit here, but oh well :)).  Technically, it only takes two lines of code/XAML to use the XapContentLoader:

First, add the following line to your Application’s startup code (usually in App.xaml.cs), which registers the pack uri scheme with the built-in Uri class, so that you can create pack Uris in code and XAML without having exceptions be thrown:

PackUriParser.Initialize();

Then, in XAML:

<navigation:Frame x:Name="ContentFrame"
                  Source="/Views/Home.xaml">
    <navigation:Frame.ContentLoader>
        <xapLoader:XapContentLoader />
    </navigation:Frame.ContentLoader>
</navigation:Frame>

And that’s it!  Alright, that’s all.  See you later…

Ok, ok, I won’t leave you hanging like that!  Let’s look at how you would now use this content loader to load a page in another xap.  First, a quick review of the pack uri scheme as I understand it:

pack://<authority>/<path>

There are three parts to this Uri:

  • “pack://” – this is the scheme name of the Uri.  All absolute pack Uris begin with this.
  • “<authority>” – this is actually another Uri (just replace “/” with “,” and escape any other necessary characters).  For the XapContentLoader, this is a path to some xap.  For example, I might use the following authority: “http:,,www.davidpoll.com,xaps,MyApplication.xap”.  This indicates that I’ll be loading a page within the xap at that location.  I also support two other “special” authorities: “application:///” and “siteoforigin://”.  “application:///” cannot have an additional path attached to it, since I translate this as the location of the initial xap from which the application was loaded.  It’s basically equivalent to Application.Current.Host.Source.  “siteoforigin://” is replaced with the directory from which the application’s xap was loaded.  As such, any of the following authorities would be valid:
    • http:,,www.davidpoll.com,xaps,MyApplication.xap
    • https:,,www.davidpoll.com,securexaps,MySecureApplication.xap
    • application:,,,
    • siteoforigin:,,MyApplication.xap (equivalent to the first item in this list if MyApplication.xap was the entry point xap)
  • “<path>” – this is the path within the xap.  Usually, this looks like: “/SomeLibrary;component/Views/Page1.xaml”.  If no assembly name is provided, I assume they are referring to the “EntryPointAssembly” referred to in the AppManifest.xaml file of the xap.

Some examples of valid Uris to navigate to (all equivalent assuming MyApplication.xap was the initial xap):

  • /Views/Page1.xaml
  • pack://application:,,,/Views/Page1.xaml
  • pack://application:,,,/MyApplication;component/Views/Page1.xaml
  • pack://http:,,www.davidpoll.com,xaps,MyApplication.xap/Views/Page1.xaml
  • pack://http:,,www.davidpoll.com,xaps,MyApplication.xap/MyApplication;component/Views/Page1.xaml
  • pack://siteoforigin:,,MyApplication.xap/Views/Page1.xaml
  • pack://siteoforigin:,,MyApplication.xap/MyApplication;component/Views/Page1.xaml

Unfortunately, coming up with these Uris can be a bit of a pain, so I’ve provided a custom type of UriMapping that works on a per-xap basis to make this simpler:

<navigation:Frame.UriMapper>
    <navUtils:UriMapper>
        <xapLoader:PackUriMapping XapLocation="siteoforigin://TernaryXap.xap"
                                  Uri="/remote/{assemblyname}/{path}"
                                  MappedPath="/{path}" />
    </navUtils:UriMapper>
</navigation:Frame.UriMapper>

Now, I needn’t ever actually write a pack uri myself.  Instead, I can have hyperlinks like “/remote/TernaryXap/Views/Page1.xaml”, and these mappings will generate the appropriate pack uris for you.  They work just like the UriMapping built into the SDK in terms of replacing values in the MappedPath.  “{assemblyname}” is a special token (that you could omit if you set the PackUriMapping.AssemblyName property) that allows you to make the assembly name part of your path.

Enhancing the experience

Great, so now we’ve got navigation integrated with on-demand xap loading.  It takes very few lines of code, and is quick to set up.  Note that my xaps have no concrete knowledge of one another (even the hyperlinks are just text, and users could manually enter other hyperlinks).  But web requests are slow, and could fail.  How can we improve the experience around this type of navigation?

Well, I’ve got two ideas for you that you might recognize: BusyIndicator and ErrorPageLoader.

Since web requests can fail (lost connectivity, bad links, servers being down, or other random problems), we should make sure users don’t see exceptions under those circumstances.  For this, we’ll whip out the ErrorPageLoader from our trusty utility belt and use it to load a local error page if something goes wrong:

<loaders:ErrorPageLoader>
    <loaders:ErrorPageLoader.ContentLoader>
        <xapLoader:XapContentLoader />
    </loaders:ErrorPageLoader.ContentLoader>
    <loaders:ErrorPageLoader.ErrorContentLoader>
        <navigationLoader:PageResourceContentLoader />
    </loaders:ErrorPageLoader.ErrorContentLoader>
    <loaders:ErrorPage ErrorPageUri="/Views/ErrorPage.xaml" />
</loaders:ErrorPageLoader>

You’ll notice that I’ve used the built-in (SDK) PageResourceContentLoader as my ErrorContentLoader.  I did this because, presumably, something bad has happened with the XapContentLoader, so I want to use a ContentLoader with low likelihood of failure in order to display an error page.

Next, users shouldn’t be left hanging while they wait for a link to load.  We can use the BusyIndicator control in the toolkit in order to let the user know that something is going on.  The XapContentLoader has an “IsBusy” property as well as a “Progress” property.  We can make the BusyIndicator’s progress bar display progress and appear by binding it to these properties on the XapContentLoader.  The following handy XAML accomplishes this:

<toolkit:BusyIndicator IsBusy="{Binding ContentLoader.ContentLoader.IsBusy, ElementName=ContentFrame}"
                       DisplayAfter="0:0:0.1">
    <toolkit:BusyIndicator.BusyContent>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <TextBlock Text="Loading page..." Margin="0,0,0,4" />
            <ProgressBar Grid.Row="1"
                         Minimum="0"
                         Maximum="1"
                         Height="25"
                         Value="{Binding ContentLoader.ContentLoader.Progress, ElementName=ContentFrame}" />
        </Grid>
    </toolkit:BusyIndicator.BusyContent>
    <toolkit:BusyIndicator.ProgressBarStyle>
        <Style TargetType="ProgressBar">
            <Setter Property="Visibility" Value="Collapsed" />
        </Style>
    </toolkit:BusyIndicator.ProgressBarStyle>
    <navigation:Frame x:Name="ContentFrame"
                      Source="/Views/Home.xaml">
        <navigation:Frame.ContentLoader>
            <loaders:ErrorPageLoader>
                <loaders:ErrorPageLoader.ContentLoader>
                    <xapLoader:XapContentLoader />
                </loaders:ErrorPageLoader.ContentLoader>
                <loaders:ErrorPageLoader.ErrorContentLoader>
                    <navigationLoader:PageResourceContentLoader />
                </loaders:ErrorPageLoader.ErrorContentLoader>
                <loaders:ErrorPage ErrorPageUri="/Views/ErrorPage.xaml" />
            </loaders:ErrorPageLoader>
        </navigation:Frame.ContentLoader>
    </navigation:Frame>
</toolkit:BusyIndicator>

Note that I’ve hidden the BusyIndicator’s default progress bar, and added one to its BusyContent that binds to the XapContentLoader’s progress.  Now, when users navigate to pages and have to wait for them to load, they are presented with the BusyIndicator and a progress bar letting them know how much is left to be downloaded.  The application continues to appear responsive, and users can continue to work with the rest of the application.

Cross-domain considerations and limiting access

We’ve now unleashed a lot of power.  Your application can now load pages in any xap that Silverlight can get access to using a WebClient.  If the Frame control you’re using integrates with the browser, users can type any uri into the browser to navigate to, and right now, the XapContentLoader will blindly go and load their code.

This could be a bit of a security issue (I’m no expert, but this one sticks out like a sore thumb).

Suppose domain supersecret.com has a cross-domain policy that allows my domain (and only my domain), davidpoll.com, to access services on it.  My domain is using the XapContentLoader, and can load pages in any xap that WebClient can access.  A developer whose domain is evil.com realizes this, and decides to try to get access to supersecret.com’s services.  He’s able to do this by:

  • Adding a cross-domain policy that allows davidpoll.com to access xaps on evil.com
  • Pointing the Silverlight application on davidpoll.com to his xap on evil.com (this will work, since WebClient just checks evil.com’s cross-domain policy)
  • Now that evil.com’s xap has been loaded and is running on davidpoll.com’s application, he can access supersecret.com’s services (since the evil.com’s code is now running on davidpoll.com)!

Because of this problem, I’ve disabled cross-domain loading on the XapContentLoader by default.  You can re-enable it, but please be aware of this potential problem.  The AuthContentLoader is a convenient way to restrict access to domains you don’t want users to be able to access just by changing the Uri.  Here, I’ve enabled cross-domain access, but restricted access only to my other domain (this one happens to be my personal website that I don’t update too often :)):

<auth:AuthContentLoader>
    <auth:AuthContentLoader.ContentLoader>
        <xapLoader:XapContentLoader EnableCrossDomain="True" />
    </auth:AuthContentLoader.ContentLoader>
    <auth:NavigationAuthorizer>
        <auth:NavigationAuthRule UriPattern="pack://((siteoforigin:,,)|(authority:,,)|(http:,,open\.depoll\.com)).*">
            <auth:Allow Users="*" />
        </auth:NavigationAuthRule>
        <auth:NavigationAuthRule UriPattern="pack://.+:,,.*">
            <auth:Deny Users="*" />
        </auth:NavigationAuthRule>
    </auth:NavigationAuthorizer>
</auth:AuthContentLoader>

This allows users to access xaps on my site (open.depoll.com), while denying access to other pack uris.  Coupled with the ErrorContentLoader, a consistent, safe experience can be provided for users.

MEF and Prism

At this point, you might be asking yourself why I rolled my own solution to this problem?  MEF and Prism both have very effective implementations for modularization of applications, and you could certainly build very similar ContentLoaders based upon their libraries for loading xaps (in fact, I hope to see folks out there do so! :)).  Really, the reason I rolled my own libraries for this is twofold:

  • Support for cached assemblies – I wanted to make sure to attempt to solve the shared dependency problem described earlier
  • Size – it’s important to keep the size of the bootstrapping assemblies small in order to reap the benefits of dynamic loading.  The total size of the libraries required for the XapContentLoader is about 25k compressed.  I’d still like to pare that down, so if folks have ideas, let me know :)

If you’ve got ideas for how best to use MEF or Prism with INavigationContentLoader, let me know!  If you’re already using them for modularization and want to integrate them with navigation, this is possible in Silverlight 4 thanks to INavigationContentLoader.

The goods

As you know by now if you’ve been following my blog, I never leave you stranded without code and a live sample!  Take a look!

An application using XapContentLoader

Click around using the links on top and see what happens to the Uri in the browser’s address bar.  You’ll note the cross-domain access as well as the use of QueryStrings (which still work, of course!).  Open up Fiddler or some other tool to verify that xaps and zip files for cached assemblies are only downloaded once thanks to browser caching.  Enjoy it!

Finally, some source code for you:

  • Live Sample (source -- found in the SLaB v0.3 source under "ScratchApplication")
  • SLaB v0.3 (includes source, a sample app, some tests, and binaries)
    • For the latest version, please check out SLaB on my Downloads and Samples page.
    • The v0.3 download of SLaB includes the following changes:
      • Added more extensible UriMapper that replicates the built-in UriMapper's behavior but allows UriMappings to be more extensible.
      • Added UiUtilities, the first of which allows you to execute a function on the UI thread and block the calling thread until it has completed (safe to call even from the UI thread)
      • Added XapLoader, which downloads a Xap and any "cached assemblies"
      • Added XapContentLoader, which uses the XapLoader to navigate to pages in downloaded assemblies
      • Added PackUri utilities so that pack Uris can be used to download and navigate to pages in Xaps
      • Signed all assemblies (public binaries use a private key, another key is distributed with source)
      • Added a build task that will generate extmap files for all libraries -- now assembly caching works with SLaB assemblies
      • Fixed a bug with the AuthLoader where all rules would be run, even if one rule already allowed access
      • Other minor bugfixes

    Enjoy, and let me know if you have any questions, thoughts, or ideas!

    Remember, SLaB is just a collection of the samples and experimental components I’ve been putting together so that they’re all in one place.  I can’t make any guarantees about maintaining them, fixing bugs, not making breaking changes, etc., but you’re more than welcome to try them out, use them, and let them inspire your development (or show you what not to do if you really dislike something I’m doing!) :).