Delen via


Prism 4 in Silverlight with MEF CompositionInitializer

I’ve had a couple people ping me about how to use the MEF CompositionInitializer with Prism 4 in Silverlight as well as how to use multiple, remotely downloaded Prism modules with MEF.  I’ve put together a sample that shows both these things, you can find the sample below.

CompositionInitializer

The MEF CompositionInitializer is intended to provide a way to satisfy MEF imports on an object in situations where you can’t necessarily build the item in the container.  This may occur where you’re using a framework that builds the objects for you, but not in the container of your choice such as in in the application startup logic of a Silverlight application. 

You might use it like this in your Silverlight App.xaml.cs if you wanted to import  something like a logger or main view-model into your App.

 private void Application_Startup(object sender, StartupEventArgs e)
 {
     CompositionInitializer.SatisfyImports(this);
  
 }
  
 ...
  
 [Import]
 public MainViewModel MainViewModel
 {
     get;
     set;
 }

You can read a bit more about CompositionInitializer on MSDN and on the MEF Codeplex site.

Under the covers, the CompositionInitializer is creating its own container and making it available through the CompositionInitializer static interface.  Since Prism also creates it’s own composition container, both Prism and the CompositionInitializer static interfaces need to point to the same container to be able to work together well.   You can setup this connection in Prism bootstrapper you create for your app by calling CompositionHost.Initialize.  This initializes the CompositionHost with the container that Prism uses of instead of creating a new one.  Perhaps the easiest place to do this in the CreateContainer override of your Prism application bootstrapper:

 public class AppBootstrapper : MefBootstrapper
 {
 ...
     protected override CompositionContainer CreateContainer()
     {
         var container = base.CreateContainer();
  
         // Initialize the CompositionHost so we can use CompositionInitializer
         CompositionHost.Initialize(container);
         return container;
     }
 ...
 }

 

Sample With Composition Initializer

The attached sample application shows a Prism application using CompositionInitializer in two places: in the app startup and in a view’s constructor.  In the App, it uses the initializer to import an ILoggerFacade so it can log the start and exit of the application.  It also shows a view that, in its constructor, uses CompositionInitializer to import its view-model.

The App.xaml.cs starts the bootstrapper (which calls the CompositionHost.Initialize as shown above) then satisfies imports on itself which injects the ILoggerFacade:

 private void Application_Startup(object sender, StartupEventArgs e)
 {
     new AppBootstrapper().Run();
     CompositionInitializer.SatisfyImports(this);
     Logger.Log("Application starting", Category.Info, Priority.Low);
 }
 [Import]
 public ILoggerFacade Logger
 {
     get;
     set;
 }

After the SatisfyImports call, the Logger dependency will have been resolved and the App can then use that dependency.  Similarly, the InjectedViewWithInitializer uses the CompositionInitializer.SatisfyImports to inject it’s view-model dependencies:

     public partial class InjectedViewWithInitializer : UserControl
     {
         public InjectedViewWithInitializer()
         {
             InitializeComponent();
  
             // Using CompositionInitializer to satisfy imports for this view.
             // Because the AppBootstrapper called CompositionHost.Initialize()
             // the container is the same as the one built up by Prism.
             CompositionInitializer.SatisfyImports(this);
         }
  
         [Import(typeof(TimerViewModel))]
         public object ViewModel
         {
             get { return this.DataContext; }
             set { this.DataContext = value;}
         }
     }

 

Prism, MEF, and Shared Services

When sharing these services across remotely downloaded modules, there is not much difference from doing this with MEF than with a container like Unity. For this sample, there is a service module that offers an implementation of a TimeService that simply returns the current time and two modules that provide views consuming the time service.  Here’s what it looks like:

image

Not too fancy, but it hopefully demonstrates the point.  ModuleA and ModuleB are dependent upon the TimeServiceModule.  ModuleA provides two views:  one view uses CompositionInitializer to resolve it’s view-model and one view receives it’s view-model when it’s constructed in the MEF container by the RegionManager.  ModuleB provides a view that directly imports the TimeService. 

image

The modules don’t directly know about each other.  The ITimeService implementation is in a shared assembly.  The Prism ModuleCatalog.xaml defines the dependencies between the modules and specifies that all modules are downloaded in the background and initialized when available:

     <Modularity:ModuleInfoGroup>
         <Modularity:ModuleInfo Ref="TimeService.xap" InitializationMode="WhenAvailable" ModuleName="TimeService"  />
         <Modularity:ModuleInfo Ref="ModuleA.xap" InitializationMode="WhenAvailable" ModuleName="ModuleA">
             <Modularity:ModuleInfo.DependsOn>
                 <sys:String>TimeService</sys:String>
             </Modularity:ModuleInfo.DependsOn>
         </Modularity:ModuleInfo>
         <Modularity:ModuleInfo Ref="ModuleB.xap" InitializationMode="WhenAvailable" ModuleName="ModuleB">
             <Modularity:ModuleInfo.DependsOn>
                 <sys:String>TimeService</sys:String>
             </Modularity:ModuleInfo.DependsOn>
         </Modularity:ModuleInfo>
     </Modularity:ModuleInfoGroup>

 

During ModuleA initialize, the InjectedViewWithInitializer is constructed and added directly to the region on the shell:

 public void Initialize()
 {
     logger.Log("Initializing ModuleA", Category.Info, Priority.Low);
  
     // We create a new instance of InjectedViewWithInitializer and add it to the region.
     // Inside InjectedViewWithInitializer, it uses CompositionInitializer.SatisfyImports(this)
     // to resolve its dependencies.
     // Because we initialize the CompositionHost in the bootstrapper with the container
     // that Prism will use, it can get all the services registered with Prism
     this.regionManager.Regions["ContentRegion"].Add(new InjectedViewWithInitializer());
  
     // Another approach is to use the view registry which will automatically build
     // the type up in the container:
     this.registry.RegisterViewWithRegion("ContentRegion", typeof(RegionRegisteredView));
 }

Module A also shows another approach to building a view against a region, one more traditionally used in Prism .  This registers a view’s type against a region so that the view is built  when the region is shown (also known as view discovery).  Since the view is built in the MEF container during this process, any of it’s [Import] or [ImportingConstructor] dependencies are satisfied.

Module B shows another view that relies on the TimeService merely to demonstrate multiple modules dependent on the same service.  In this sample, the ITimeService interface actually lives in a separate Shared assembly while the exported implementation lives in TimeService assembly.

One thing to be wary of with MEF and using remotely downloaded modules in Prism is having more than one module reference the same assembly that include [Export] attributes.  By default, adding a reference to an assembly will set the CopyLocal property to True which means a copy of the referenced assembly is included in the modules Xap file.  When using MEF, Prism uses the DeploymentCatalog to retrieve the module Xaps.  As these Xaps are downloaded, the DeploymentCatalog will add these assemblies to the MEF container.  If more than one Xap includes the same assembly, it will be exported multiple times.  This may result in a MEF recomposition error like the one shown below if the export cannot be ‘recomposed’ (more information about MEF DeploymentCatalog and recomposition is available on the MEF Codeplex site.):

The composition remains unchanged. The changes were rejected because of the following error(s): The composition produced a single composition error. The root cause is provided below. Review the CompositionException.Errors property for more detailed information.

1) Change in exports prevented by non-recomposable import 'ModuleA.TimerViewModel..ctor (Parameter="timeService", ContractName="ServiceModule.ITimeService")' on part 'ModuleA.TimerViewModel'.

Check the InnerException property of the exception for more information. If the exception occurred while creating an object in a DI container, you can exception.GetRootException() to help locate the root cause of the problem.

You can solve this by making sure the assembly is included in only one Xap file by setting the CopyLocal property to False for all but a single Xap.

Compiling the Sample

You will need to download and reference the Prism binaries from the MSDN web site. You may also need to set _MainApplication.Web as the startup project.

PrismMefSatisfyImports.zip

Comments

  • Anonymous
    January 10, 2011
    Hi,Thanks for the example! I want to make the shared TimeService available in App.xaml.cs.Adding a reference[Import]       public ITimeService ts { get; set; }to App class  create a "No valid exports were found that match the constrain..." errorWhat need to be done to access the remote serivce module via its interface ( ITimeService ) ? I've tried several examples with or without CompositionInitializer(), they produce the same error of not finding the exports. It will be fine if I load the module explicitely from code via AggregateCatalog, but not through deployment catalog in XML. Why?Regards,Reggie
  • Anonymous
    January 12, 2011
    @Reggie:Use something similar like the following code in your bootstrapper to create your AggregateCatalog manually:protected override void ConfigureAggregateCatalog()       {           base.ConfigureAggregateCatalog();           AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(AppBootstrapper).Assembly));           AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(StatisticViewModelBase).Assembly));       }Greetings, Michael
  • Anonymous
    April 14, 2011
    Hey brumfb1,Thanks for simple and useful article. I needed that. I do have problem though. In my project when I try to inject view in module's Initialize() method with this.RegionManager.Regions[RegionNames.MainNavigationRegion].Add(new FillRateNavigationItemView());I get an exception because regions are not created. I think that I implemented everything like in your example. I do have one diference though. I am using ConfigureAggregateCatalog like in a example above instead CreateModuleCatalog like in your example. I am not even sure why I do that.Would you guys know why the regions are not created at that time?Thanks, Dragan