Silverlight 3 Navigation: Dynamically Loaded Pages… Now MEF Powered!

Recently David Poll posted a very cool technique for navigating to dynamically loaded pages and on demand downloading of pages in a Silverlight 3 application .  It was very cool but the explicit wire up of each page can be a pain in large applications.  So having just posted about MEF on Silverlight, I thought this was a great fit.  The Managed Extensibility Framework (MEF) is all about discovering and wiring up components and Silverlight 3 Navigation has some very interesting components.    But before I could get it done, our Test Manager, Dinesh Chandnani beat me to it.  He wrote this very cool example app that I extended just a bit.

Download the all the source code and check out an live sample

The model is very simple.  You just create a page (via the Silverlight Page item template) and put metadata on them saying how to navigate to it.  The page can be in the same XAP or different XAP.  No need to wire them up.   

For example, for a page in the same XAP, you just create a page and add some metadata to it

    1: [PageMetadata(Content = "page1", NavigateUri = "/page1")]
    2: public partial class Page1 : Page
    3: {

For a page in a different assembly, possibly delay loaded or conditionally loaded:

    1: [PageMetadata(Content="delayloaded",
    2:     NavigateUri="/DelayLoadedPages;component/DelayLoadedPage.dyn.xaml")]
    3: public partial class DelayLoadedPage : DynamicPage
    4: {

Pretty much the same, but we use the packURI syntax and David Poll’s Dynamic page class\pattern (see his blog post above for more information).

If you want to load pages from an external assembly, you do need to explicitly load that assembly.  But this is pretty easy to do and it can be done at any point in the application.  When the user logs in as in an admin role, when advanced functionality is required, after initial startup of the application, etc.  The page simply appears in the nav bar when it is downloaded and running.   For example, I extended Dinesh’s code by adding an assembly that was loaded when a button is clicked.  The code is simple:

    1: private void Button_Click(object sender, RoutedEventArgs e)
    2: {
    3:     Package.DownloadPackageAsync(new Uri("DelayLoadedPages.xap", 
    4:         UriKind.Relative), DownloadCompleted);
    5:  
    6: }
    7: private void DownloadCompleted(AsyncCompletedEventArgs e, Package p)
    8: {
    9:     var app = App.Current as SilverlightMEFNavigation.App;
   10:  
   11:     if (!e.Cancelled && e.Error == null)
   12:         app.PackageCatalog.AddPackage(p);
   13: }

When the button is clicked, we download kick off an async download of the XAP (line 3-4).  Then when it is downloaded it we add it to the catalog the nav bar is tied to in line 12. 

 

Now, how does all this work?  There are afew parts really. 

First we define the PageMetadataAttribute in the main assembly.  All the pages will have to reference this, so you might want to factor it out into its own assembly..

    1: [MetadataAttribute]
    2: [AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
    3: public class PageMetadataAttribute  : ExportAttribute
    4: {
    5:     public PageMetadataAttribute() : base(typeof(FrameworkElement)) { }
    6:     public string NavigateUri { get; set; }
    7:     public string Content { get; set; }
    8:     
    9: }

A few notes:

Line 1: We set this up as a MEF Metadata attribute.. this enables the MEF engine to get composition information from this attribute

Line 2: We set this up such that it can be applied only once per type.  MEF uses this as a hit on how to compose it.

Line 3: we derive from ExportAttribute, this enables us to completely hide the MEFness out of the picture…  folks writting pages don’t need to know MEF is under the covers at all. 
Line 4: Say that this attribute will be used on FrameworkElements, a hit to MEF on how to type things. 
Lines 6-7: just set up the attribute interface we want.

 

In MainPage.cs, we have some code that populates the nav bar as new pages are discovered. 

    1: [Export]
    2: public partial class MainPage : UserControl, IPartImportsSatisfiedNotification
    3: {
    4:     [ImportMany(AllowRecomposition = true)]
    5:     public ObservableCollection<Lazy<FrameworkElement, IPageMetadata>> Pages = null;

We have a Pages collection that we have marked with AllowRecomposition, which means its value’s can change over the life of the application (such as when a new package is added as I showed above).

Then we simply update the links whenever new items are available in the Page collection.

    1: private void UpdateLinks()
    2: {
    3:     if (Pages == null || Pages.Count <= 0)
    4:         return;
    5:  
    6:     for (int i = 0; i < Pages.Count; i++)
    7:     {
    8:         if (LinksStackPanel.Children.Where(u=>(u is HyperlinkButton) 
    9:             && (u as HyperlinkButton).Content.ToString() 
   10:                     == Pages[i].Metadata.Content).FirstOrDefault() != null)
   11:             continue;
   12:         HyperlinkButton hb = new HyperlinkButton();
   13:         hb.Style = ((App)Application.Current).Resources["LinkStyle"] as Style;
   14:         hb.Content = Pages[i].Metadata.Content;
   15:         hb.NavigateUri = new Uri(Pages[i].Metadata.NavigateUri, UriKind.Relative);
   16:         LinksStackPanel.Children.Add(hb);
   17:     }
   18: }

Notice in line 12-16 we are dynamically creating the Xaml elements to add into the tree. 

The final bit of code we added was in App.cs to set MEF..

    1: private void Application_Startup(object sender, StartupEventArgs se)
    2: {
    3:     AggregateCatalog aggregateCatalog = new AggregateCatalog();
    4:     AssemblyCatalog ac = new AssemblyCatalog(Assembly.GetExecutingAssembly());
    5:     aggregateCatalog.Catalogs.Add(ac);
    6:     aggregateCatalog.Catalogs.Add(PackageCatalog);
    7:     // Load any package dynamically
    8:     Package.DownloadPackageAsync(new Uri("AdditionalPages.xap", UriKind.Relative), DownloadCompleted);
    9:     CompositionContainer container = new CompositionContainer(aggregateCatalog);
   10:     this.RootVisual = container.GetExportedValueOrDefault<MainPage>();
   11: }

Here we setup the catalog to enable MEF to look for pages in the current assembly and in an assembly we download dynamically after the app is up and running. Line 8 sets up the call to the handler below when the page has been downloaded…

    1: private void DownloadCompleted(AsyncCompletedEventArgs e, Package p)
    2: {
    3:     Thread.Sleep(1000);
    4:     if (!e.Cancelled && e.Error == null)
    5:         PackageCatalog.AddPackage(p);
    6: }

For such as simple example on my dev machine the page downloads immediately so you can’t really tell.. so I added a brief delay so you can see it pop it easier.   So the Thread.Sleep() in line 3 is there to demo better…  I highly recommend removing it in production code.

 

That is all there is to it!  A very simple solution. 

 

For more information see: