Freigeben über


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:

Comments

  • Anonymous
    July 31, 2009
    Thanks Brad, These sorts of posts are really handy to learn how all the pieces fit together. I still have a lot of reading to go to understand how to take full advantage of MEF but it seems like this is the perfect solution in my current App, whereby only certain pages are available depending on the users role.

  • Anonymous
    August 01, 2009
    Hi Brad, I wanted to thank you for all these great posts on SL 3. They are extremely helpful and greatly appreciated. bill

  • Anonymous
    August 01, 2009
    The comment has been removed

  • Anonymous
    August 01, 2009
    The code download does not match the live sample. The product page is missing in the download.

  • Anonymous
    August 02, 2009
    This is great, but it highlights even more the overlap between MEF and Prism - which makes it really hard to decide which to adopt! Any tips?

  • Anonymous
    August 02, 2009
    >The code download does not match the live sample. >The product page is missing in the download. Fallon Massey - Good call.. I had left some code commented out..  Sorry, I just uploaded a new zip.. that should fix it.  

  • Anonymous
    August 02, 2009
    So far the menu samples that I have seen have only a few fixed items... like 5.  I have projects that have the menu items that list down the left side and may need a scrollbar to see them all. Maybe 20 to 30 items.... and then after clicking them, there are sublists of 20-30 and sub-sublists. We take these lists from the database, filter by user access before showing them. When an item is clicked then we would like to truly dynamically load the .xap.... Would be nice to see how the navigation system can scale in this way.

  • Anonymous
    August 11, 2009
    Hi Brad I have a rather slow connection. How long is it taking for this app to load on your machine? This is what I've been wanting sense my first SL 2 program. Thank for all the great work.

  • Anonymous
    September 03, 2009
    Brad, I am using DownloadPackageAsync to download the package.  How can I get the content files (such as a settings.xml) within the downloaded package.  The package exposes an Assemblies collection only. Also, is there a way to persist the package locally (isolated storage). Thank you. Jun

  • Anonymous
    September 06, 2009
    > How can I get the content files (such as a >settings.xml) within the downloaded package.   Jun, you are right, this is a limitation in the current release..  the only real workaround is to embed the resource into the assembly and read it from there.

  • Anonymous
    September 15, 2009
    The comment has been removed

  • Anonymous
    September 16, 2009
    Great example. Three questions:

  1. Why does does the debugger never stop in the AdditionalPages (breakpoints never get enabled) but does stop in the DelayLoadedPages. Both are dynamically loaded.
  2. If a ServiceReferences.ClientConfig is coming with a XAP, required for the configuration of a webservice, the assembly would not be able to find it. Now as you suggested, it could be included in the resource as well, but then the binding and endpoint would have to be read from the resource in order to create the binding and pass it on the constructor of the webservice. But that seems to be impossible if you would like to use the custombinding with binary encoding. Any suggestions?
  3. The 'NavigatedTo' method seems never to be called, where would that be called from normally? Tnx  Ben
  • Anonymous
    September 16, 2009
    BTW Jun Miao, you could just inculde the settings.xml in the XAp (it;s just a zip file) and once the XAP is downloaded, you can read it from the XAP. Same actually fot the ServiceReferences.ClientConfig.

  • Anonymous
    October 22, 2009
    Brad, Great, it works really fine. I am new in SL and maybe this is a stupid question, but I do not know how to access to the controls of the page. For example: <controls:TreeView x:Name="tvReports"  ... I cannot refer to tvReports from the derived DynamicPage class. If I derive my class from UserControl, of course it works fine. Do you know how to access to a control placed in the view? Thank you