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:
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.
Microsoft Azure and Amazon Web Services (AWS) are two of the most popular cloud platforms.…
Cloud management is difficult to do manually, especially if you work with multiple cloud…
Azure’s scalable infrastructure is often cited as one of the primary reasons why it's the…
https://www.youtube.com/watch?v=wDzCN0d8SeA Watch our "Unlocking the Power of AI in your Software Development Life Cycle (SDLC)"…
FinOps is a strategic approach to managing cloud costs. It combines financial management best practices…
Using Kubernetes with Azure combines the power of Kubernetes container orchestration and the cloud capabilities…
View Comments
A big thank you for this, I am sure to use it, or a variation, in my future projects.
PingBack from http://topsy.com/training.atmosera.com/CS/blogs/jprosise/archive/2010/06/27/dynamic-page-loading-in-silverlight-navigation-apps.aspx?utm_source=pingback&utm_campaign=L2
Thanks Jeff, this is great. One thing I added was trimming params off the Uri and using that as my key for the _pages dictionary. That way, you only create the type once for Page1?ID=1 and Page1?ID=2, etc.
FYI, Glenn Block does something similar with MEF, but I like this a little better since his works with full .xaps instead of .dlls and you get multiple copies of common referenced .dlls on the client (which actually screws up MEF if you have an Export in the common .dll and you want a single Import somewhere).
http://live.visitmix.com/MIX10/Sessions/CL52
http://cid-f8b2fd72406fb218.office.live.com/self.aspx/blog/MIX%202010%20Demos.zip
That's a great point about the query strings. I'll add that in to my implementation, too. I may also add support for XAPs so you can package resources in XAPS or DLL, whichever you prefer.
Hey Jeff, this is great! :) I'm glad to see others are finding useful things to do with INavigationContentLoader.
This actually looks very similar in functionality to one of the posts I did a while back: http://www.davidpoll.com/2010/02/01/on-demand-loading-of-assemblies-with-silverlight-navigation-revisited-for-silverlight-4-beta/
In my case, I used pack Uris (or a funky variation on them) to produce a very similar result. I don't support grabbing Pages from raw dlls, but if they're in a XAP, I'll handle them.
Anyhow, keep it up -- I look forward to seeing what you do next with it!
-David
Thanks for sharing your knowledge
Jeff, this is very cool!!! Now, I'm trying to insert an "IsBusy" property in it... and so, bind it to a BusyIndicator... but I'm not getting this...
Could you help please?
Great Jeff ! Thanks
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.
Could you explain then note:
"Assumed that these properties are accessed from a single thread" in NavigationAsyncResult.
I'm a beginner.
Thanks