Paritioning applications across multiple XAPs

The default Silverlight programming model requires all MEF parts to be in the current XAP. This is fine for simple Silverlight composable applications. It poses severe problems for very large applications:
  1. Causes the default XAP to continually bloat which increases the initial app download and hinders the user's startup experience.
  2. Prevents Silverlight applications from offering an extensibility experience similar to the desktop. In many scenarios it is ideal to allow applications to be extended on the server.
  3. For multiple teams on a large app it makes it difficult to add new functionality to the application.
MEF offers a solution to both through a new catalog called System.ComponentModel.Composition.Hosting.DeploymentCatalog located in the Initialization dll.

DeploymentCatalog

DeploymentCatalog downloads parts from XAPs living on the web server. Using it you can break your application into as many XAPs as you like and use it to bring them back together. DeploymentCatalog downloads catalogs asynchronously and implements the async event pattern in order to allow you to monitor the download i.e. track completion, failures, etc. Because DeploymentCatalog is recomposable hower it is not required for you to do this, but we recommend you do. To use DeploymentCatalog with CompositionInitializer you must override the default configuration using CompositionHost otherwise CompositionInitializer will never see the downloaded parts.

Downloading parts lazily once the app starts.

The most common (and simplest) scenario for using DeploymentCatalog is to reduce your app startup footprint and immediately start downloading other pieces in the background. For example below (hardcoding of modules is for illustration but not necessary) the order management system starts up with only the Home and Order modules present (not shownin the code). It immediately initiates download of the "Admin", "Reporting" and "Forecasting" modules which are not required initially. Importing a ViewModel dropped for simplicity. Below is a diagram that indicates the design.

Partitioned App.png

Here is a code snippet that shows how this will work.

public class App : Application {
  public App() {
    this.Startup += this.Application_Startup;
    this.Exit += this.Application_Exit;
    this.UnhandledException += this.Application_UnhandledException;

    InitializeComponent();
  }

  private void Application_Startup(object sender, StartupEventArgs e)
  {
    var catalog = new AggregateCatalog();
    catalog.Catalogs.Add(CreateCatalog("Modules/Admin"));
    catalog.Catalogs.Add(CreateCatalog("Modules/Reporting"));
    catalog.Catalogs.Add(CreateCatalog("Modules/Forecasting"));

    CompositionHost.Initialize(new DeploymentCatalog(), catalog);
    CompositionInitializer.SatisfyImports(this)

    RootVisual = new MainPage();
  }

  private DeploymentCatalog CreateCatalog(string uri) {
    var catalog = new DeploymentCatalog(uri);
    catalog.DownloadCompleted += (s,e) => DownloadCompleted();
    catalog.DownloadAsync();
    return catalog;
  }

  private void DownloadCompleted(object sender, AsyncCompletedEventArgs e) {
    if (e.Error != null) {
      MessageBox.Show(e.Error.Message);
    }
  }

  private Lazy<IModule, IModuleMetadata>[] _modules;

  [ImportMany(AllowRecomposition=true))]
  public Lazy<IModule, IModuleMetadata>[] Modules {
    get{return _modules;}
    set{
      _modules = value;
      ShowModules()
  }

  private void ShowModules() {
    //logic to show the modules
  }

}
Public Class App
    Inherits Application

    Public Sub New()
        InitializeComponent()
    End Sub

    Private Sub Application_Startup(ByVal sender As Object, ByVal e As StartupEventArgs) 
        Dim catalog = New AggregateCatalog()
        catalog.Catalogs.Add(CreateCatalog("Modules/Admin"))
        catalog.Catalogs.Add(CreateCatalog("Modules/Reporting"))
        catalog.Catalogs.Add(CreateCatalog("Modules/Forecasting"))

        CompositionHost.Initialize(New DeploymentCatalog(), catalog) 
        CompositionInitializer.SatisfyImports(Me) 
        RootVisual = New MainPage()
    End Sub


    Private Function CreateCatalog(ByVal uri As String) As DeploymentCatalog
        Dim catalog = New DeploymentCatalog(uri) 
        AddHandler catalog.DownloadCompleted, Sub(s, e) DownloadCompleted()
        catalog.DownloadAsync()
        Return catalog
    End Function

    Private Sub DownloadCompleted(ByVal sender As Object, ByVal e As AsyncCompletedEventArgs) 
        If e.Error IsNot Nothing Then
            MessageBox.Show(e.Error.Message) 
        End If
    End Sub

    Private _modules() As Lazy(Of IModule, IModuleMetadata) 

    <ImportMany(AllowRecomposition:=True)> 
  Public Property Modules() As Lazy(Of IModule, IModuleMetadata)() 
        Get
            Return _modules
        End Get
        Set(ByVal value As Lazy(Of IModule, IModuleMetadata)()) 
            _modules = value
            ShowModules()
        End Set

    End Property

    Private Sub ShowModules()
        'logic to show the modules
End Sub

End Class

There's a bunch of things going on above to make this work. First we are creating an AggregateCatalog which we are populating with 3 DeploymentCatalogs for our modules by calling a CreateCatalog helper method. CreateCatalog does the following:
  • Creates a new DeploymentCatalog passing in a relative uri. In this case "Modules/" will be off off of "ClientBin/" on the server.
  • Subscribes to the DownloadCompleted event. This is necessary primarily to track any failures that occur during the download.
  • Starts the async download
  • Returns the catalog
Next CompositionHost.Initialize is called passing in an empty DeploymentCatalog (in order to get parts in the current XAP) and the aggregate catalog which contains the DeploymentCatalogs being downloaded. Finally the app composes. The app has a single imports of all available modules which at startup will be minimally those modules included in the main XAP. Once the modules are imported, {ShowModules} is called to display them. Notice the app does not wait for the download to complete rather it composed immediately with whatever parts are present. Also notice that in the DownloadCompleted event we are not having to handle adding the new parts to the container. More on this in the next section.

Composing dynamically downloaded parts and using Recomposition.

In the code snippet above we did not wait for the new parts to show up, nor did we compose the app. This raises the question of how the new modules will show up. The answer is that the app relies on a feature of MEF called Recomposition. The modules import is marked with AllowRecomposition=true. This tells MEF that new parts are allowed to show up after the initial composition. DeploymentCatalog is recomposable meaning after the download of the XAP completes and it discovers parts, it will automatically tell MEF new parts are there. When that happens the Modules import will automatically be replaced with new newer set of modules. It will not replace the existing module instances. Thus DeploymentCatalog and Recomposition complement each other for application partitioning scenarios.

Note: Recomposition requires that existing imports of those contracts are recomposable, otherwise MEF prevents the recomposition which will result in an exception when adding a DeploymentCatalog. For example if AllowRecomposition = true is removed from Modules, then MEF will throw an exception when new modules show up.

Downloading parts on demand after startup based on user action

The previous topics discussed downloading XAPs at startup. An alternative scenario is an app which downloads parts on-demand after the app is running. In this case it is not uncommon to have other parts in the system have access to downloading new XAPs. For example in stead of downloading the above modules automatically in the previous OrderManagement scenario, you can have modules only download if needed.

In order to setup this type of configuration a bit more host setup work is required. The apis you will use are the same, with one addition. Instead of simply passing an aggregate catalog at the time of construction, you will wrap the catalog in a service which will be exported to other parts thus allowing them to pull down new XAPs. Below is sample code that illustrates how to do this through the help of a DeploymentCatalogService.

using System.ComponentModel;
using System.ComponentModel.Composition.Hosting;

public class App : Application {
  public App() {
    this.Startup += this.Application_Startup;
    this.Exit += this.Application_Exit;
    this.UnhandledException += this.Application_UnhandledException;

    InitializeComponent();
  }

  private void Application_Startup(object sender, StartupEventArgs e)
  {
    DeploymentCatalogService.Initialize();
    CompositionInitializer.SatisfyImports(this);

    var mainPage = new MainPage();
    mainPage.DataContext = ViewModel;
    RootVisual = mainPage;
  }

  [Export(typeof(IDeploymentCatalogService))]
  public class DeploymentCatalogService : IDeploymentCatalogService
  {
    private static AggregateCatalog _aggregateCatalog;

    Dictionary<string, DeploymentCatalog> _catalogs;

    public DeploymentCatalogService()
    {
      _catalogs = new Dictionary<string, DeploymentCatalog>();
    }

    public static void Initialize()
    {
      _aggregateCatalog = new AggregateCatalog();
      _aggregateCatalog.Catalogs.Add(new DeploymentCatalog());
       CompositionHost.Initialize(_aggregateCatalog);
    }

    public void AddXap(string relativeUri, Action<AsyncCompletedEventArgs> completedAction)
    {
      DeploymentCatalog catalog;
      if (!_catalogs.TryGetValue(uri, out catalog))
      {
        catalog = new DeploymentCatalog(uri);

        if (completedAction != null)
          catalog.DownloadCompleted += (s, e) => completedAction(e);
        else
          catalog.DownloadCompleted += (s,e) => DownloadCompleted;

        catalog.DownloadAsync();
        _catalogs[uri] = catalog;
        _aggregateCatalog.Catalogs.Add(catalog);
      }

      void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
      {
        if (e.Error != null)
        {
          throw new InvalidOperationException(e.Error.Message, e.Error);
        }
      }
    }
    
  }
  
  public interface IDeploymentService {
    AddXap(string relativeUri, Action<AsyncCompletedEventArgs> completedAction);
  }
Imports System.ComponentModel
Imports System.ComponentModel.Composition.Hosting

Public Class App
    Inherits Application
    Public Sub New()
        InitializeComponent()
    End Sub

    Private Sub Application_Startup(ByVal o As Object, ByVal e As StartupEventArgs) Handles Me.Startup
        DeploymentCatalogService.Initialize()
        CompositionInitializer.SatisfyImports(Me) 
        Dim mainPage_Renamed = New MainPage()
        mainPage_Renamed.DataContext = ViewModel
        RootVisual = mainPage_Renamed
    End Sub


    <Export(GetType(IDeploymentCatalogService))> 
    Public Class DeploymentCatalogService
        Implements IDeploymentCatalogService
        Private Shared _aggregateCatalog As AggregateCatalog
        Private _catalogs As Dictionary(Of String, DeploymentCatalog) 

        Public Sub New()
            _catalogs = New Dictionary(Of String, DeploymentCatalog)() 
        End Sub

        Public Shared Sub Initialize()
            _aggregateCatalog = New AggregateCatalog()
            _aggregateCatalog.Catalogs.Add(New DeploymentCatalog())
            CompositionHost.Initialize(_aggregateCatalog) 
        End Sub

        Public Sub AddXap(ByVal Uri As String, ByVal completedAction As Action(Of AsyncCompletedEventArgs)) Implements IDeploymentService.AddXap
            Dim catalog As DeploymentCatalog
            If Not _catalogs.TryGetValue(Uri, catalog) Then
                catalog = New DeploymentCatalog(Uri) 
                If completedAction IsNot Nothing Then
                    AddHandler catalog.DownloadCompleted, Sub(s, e) completedAction(e) 
                Else
                    AddHandler catalog.DownloadCompleted, Sub(s, e) DownloadCompleted()
                End If
                catalog.DownloadAsync()
                _catalogs(Uri) = catalog
                _aggregateCatalog.Catalogs.Add(catalog) 
            End If
        End Sub

        Private Sub DownloadCompleted(ByVal sender As Object, ByVal e As AsyncCompletedEventArgs) 
            If e.Error IsNot Nothing Then
                Throw New InvalidOperationException(e.Error.Message, e.Error) 
            End If
        End Sub

    End Class
End Class


Public Interface IDeploymentService
    Sub AddXap(ByVal relativeUri As String, ByVal completedAction As Action(Of AsyncCompletedEventArgs)) 
End Interface

In the sample above, a DeploymentCatalogService is introduced. It exposes a static Initialize method which is used to initializes the CompositionHost with an AggregateCatalog which it keeps a static reference to. The static member allows the exported instance of DeploymentCatalogService to access the aggregate to add new catalogs. The AddXap method takes in a uri which it uses to create a DeploymentCatalog if one does not already exist for that uri. It then subscribes to the DownloadCompleted event and starts the async download. The catalog is then immediately added to aggregate.

Handling errors during download

As DeploymentCatalog implements the async event pattern, it does not throw exceptions should any occur duing download. Should an exception occur it will raise the DownloadCompleted event and set the Error property on the args class to the Exception. You can see this in the above sample. If an error does occur, you should discard the DeploymentCatalog instance and create a new one. You cannot reset it. It will not cause any issues leaving it in the aggregate catalog however as catalogs that have not downloaded simply return an empty collection of parts.

Progress updates

DeploymentCatalog supplies continual notifications during the download. This is useful for supplying a progress bar which displays percentage of download. To receive progress updates, subscribe to the DownloadProgressChanged. Below you can see that handler for this event.

Cancelling download

In some scenarios it makes sense to allow users to cancel the download. One case might be if an application is going offline. For these situations DeploymentCatalog supports a CancelAsync method. Once you call this method you cannot reuse that catalog. To resume the download you should create a new catalog.
public class DownloadManagerViewModel, INotifyPropertyChanged {

  public event PropertyChangedEventHandler PropertyChanged;

  public long BytesReceived {get;private set;}

  public int Percentage {get; private set;}

  public void DownloadProcessChanged(object sender, DownloadProgressChangedEventArgs e) {
    BytesReceived = e.BytesReceived;
    Percentage = e.ProgressPercentage;
    RaisePropertyChanged("BytesReceived");
    RaisePropertyChanged("Percentage");
  }

  public void RaisePropertyChanged(string property) {
    var handler = PropertyChanged;
    if(handler!=null) 
      PropertyChanged(new PropertyChangedEventArgs(property));
  }
}
Public Class DownloadManagerViewModel
    Implements INotifyPropertyChanged
    Public Event PropertyChanged As PropertyChangedEventHandler
    Private privateBytesReceived As Long
    Public Property BytesReceived() As Long
        Get
            Return privateBytesReceived
        End Get
        Private Set(ByVal value As Long) 
            privateBytesReceived = value
        End Set
    End Property

    Private privatePercentage As Integer
    Public Property Percentage() As Integer
        Get
            Return privatePercentage
        End Get
        Private Set(ByVal value As Integer) 
            privatePercentage = value
        End Set
    End Property

    Public Sub DownloadProcessChanged(ByVal sender As Object, ByVal e As DownloadProgressChangedEventArgs) 
        BytesReceived = e.BytesReceived
        Percentage = e.ProgressPercentage
        RaisePropertyChanged("BytesReceived")
        RaisePropertyChanged("Percentage")
End Sub

    Public Sub RaisePropertyChanged(ByVal [property] As String) 
        Dim handler = PropertyChanged
        If handler IsNot Nothing Then
            RaiseEvent PropertyChanged(New PropertyChangedEventArgs([property])) 
        End If
    End Sub
End Class

DownloadManagerViewModel exposes a TrackDownload method. DownloadManagerViewModel exposes a DownloadProgressChanged method which can be called by the application when the event is received. In the event it sets it's BytesReceived and Percentage proeprties to the event args. It then raises the PropertyChanged event in order to notify the UI.

Using DeploymentCatalog while offline / running out of browser

DeploymentCatalog builds on top of the Silverlight WebClient class. When an application is offline, that class will automatically leverage the browser cache. This means that previously downloaded XAPs can still be accessed as long as they are in the cache. When the cache is cleared, the XAPs will need to get re-downloaded.

Caveats to using DeploymentCatalog

  • Host most be configured to use it (as is shown).
  • Silverlight cached assemblies are not supported including in the main XAP (meaning MEF will not discover them or initiate the download of cached assemblies). There are two work arounds to this.
    • First you can have the application reference the assemblies that are shared. This will allow XAPs to reference these assemblies with CopyLocal set to False in the references.
    • Second you can have a Shared XAP which contains the assemblies that are shared. As long as the XAP is downloaded before the parts that need it are, then references from those XAPs (CopyLocal=False) will work. This allows you to dynamically introduce new contracts that are not statically referenced.
  • Localization is not supported. We grab only the assemblies in the XAP that are defined in the manifest.
  • Loose resources/files living outside the assembly cannot be accessed though you can use embedded resources.
  • Downloaded catalogs are not copied to the file system when it runs out of browser.

For more on DeploymentCatalog see this post: uri:http://codebetter.com/blogs/glenn.block/archive/2010/03/07/building-hello-mef-part-iv-deploymentcatalog.aspx

Last edited Aug 9, 2010 at 6:56 PM by haveriss, version 10

Comments

mokdes Nov 27, 2012 at 9:13 PM 
I'm getting this issue when I try to reload a xap file using DeploymentCatalog,
the file is reloaded successfully but the re-composition seems to use the old version of the xap file.

Here is the situation:

I have a module called ModuleA.xap that is loaded when a button is clicked on UI.
after the ModuleA.xap is loaded using DeploymentCatalog a list is populated with data that comes from a service defined within ModuleA.xap.
the service has property called Version [String] that's shows on UI.

A updated Module.xap package and assigned a new value for Version property.
I clicked the button to reload ModuleA.xap.
After recomposition I still see the old value for Version property. !!!!!!
Through Chrome I see that a request is made and the updated version of ModuleA.xap is loaded.
but when I change the assembly version re-composition fails !!!!!!!

-Hamid

chadderack Oct 27, 2011 at 10:19 PM 
Found that DeploymentCatalog wasn't working with loose .dll files... the parts would never get discovered.

This URL describes a nice work around

http://pietschsoft.com/post/2010/10/22/Silverlight-Modify-MEF-to-load-plugins-from-DLL-and-XAP-files.aspx

irodriguez Sep 22, 2011 at 4:50 PM 
Hello, I'm newbie on MEF, Actually I followed this http://devlicio.us/blogs/rob_eisenberg/archive/2010/08/21/caliburn-micro-soup-to-nuts-part-5-iresult-and-coroutines.aspx guide to load xaps while a Silverlight App is running. The question is.... Can I have access to set or get Properties from the module loaded in another xap? how can I do that?... For example, i have a MainModule and a OrdersModule, but the OrdersModule need the id of the Restaurant selected on the MainModule to get all orders information about it. I'm little confused...

Thanks in advance..

cbordeman Dec 7, 2010 at 7:35 PM 
This approach doesn't seem to work unless every import in your app is recomposable (marked AllowRecomposition = true) because when you add to the aggregate catalog, it tries to recompose everything under the sun.

Is there a way around that?

gblock May 28, 2010 at 4:03 AM 
@racole2 catalogs are available offline as long as they were previously downloaded and are in the browser cache, as DeploymentCatalog will use the cache (through WebClient). If the cache has been cleared however they will not be availalbe.

@cmichaelgraham the problem is the relationships really don't exist in VS as there are no static refs to the XAPs. Instead the importing assembly only has references contracts, but the download is in imperative code.

cmichaelgraham Apr 14, 2010 at 11:57 AM 
MEF rules. DeploymentCatalog is brillian.

I am coming from a .NET world where i can enumerate the referenced assemblies in an assembly that I just loaded. This allows me to recursively load (actually download and load) the referenced assemblies, giving me an automatic, just-in-time behavior.

If I understand the caveats (and and Silverlight AppDomain and Assembly classes), I must create my own independent data structure that will tell me the relationship between my XAPs, Assemblies, and referenced Assemblies so that I can insure that all referenced Assemblies (and therefore all dependent XAPs) have been loaded before loading the XAP containing the referencing Assembly.

I know I can do this manually, but it seems to me that the relationships are already present in the Visual Studio project. It also seems like relationships are present in the Silverlight world, but access to the relationships are blocked from the code.

This troubles me because an otherwise elegant system of composition is tarnished by inevitable configuration errors in manually reiterating the dependent relationships.

Whew !! I feel much better now that I got that off my chest.

Please let me know if I'm missing something here :)

Keep up the amazing work...
-Mike

rcole02 Apr 9, 2010 at 9:12 PM 
What are the implications of the last caveat (Downloaded catalogs are not copied to the file system...) for Out of Browser use while disconnected from the server?