MEF Simplified: Using The Managed Extensibility Framework to Dynamically Load a Silverlight .XAP
Live Example: http://silverlight.adefwebserver.com/dynamicMEF
The documentation for The Managed Extensibility Framework (MEF) describes its purpose as a tool that "...simplifies the creation of extensible applications". That is a very general statement. It's like describing nuclear physics as "the field of physics that studies the building blocks and interactions of atomic nuclei". In some ways MEF is as powerful to programming as nuclear physics is to science.
However, for the "common man", nuclear physics is only important because it makes your lights turn on when you flick the light switch. With MEF, we will use it to simply load a .xap file that contains a Silverlight application. Trust me, it can do a lot more, but MEF is in a state like the Visual State Manager was years ago, powerful but hard to use, so you may decide to wait for more advanced tooling to appear before implementing some of it's other functions.
A Quick Easy Win - For Those Having A Hard Time Understanding MEF
You may have heard about MEF and how great it is. However, you may find it a bit confusing and hard to follow. Fear not. This article is designed for you! The goal here is to give you a quick easy win. You will be able to understand this one. You will come away with something you can use, and you will be able to tell your fellow colleagues, "MEF? yeah I'm using that, good stuff".
A Little Help From My Friends - Glenn Block and Davide Zordan
This article covers material, and uses code, from Glenn Block and Davide Zordan. You will want to read their articles after this one because they go much deeper into what you can do with MEF. The difference with this article is that we will walk through all the steps and we will also use View Model Style for everything.
If you are new to View Model Style it is suggested that you read Silverlight View Model Style : An (Overly) Simplified Explanation for an introduction.
The Application
First, let us walk though the completed application:
First, we add two Silverlight applications to the ClientBin directory of the web project for the Main Silverlight Project.
When you launch the Main Silverlight Project, you can select a Silverlight .xap from the dropdown, and click the Load button.
The .xap will be dynamically loaded.
Creating the "Injected" Silverlight Applications
We will first create two Silverlight applications. They will each produce a .XAP file that will be placed in the ClientBin directory of the Main Silverlight Project. The Main Silverlight project with then dynamically load them, and show them.
Open Microsoft Expression Blend 4, and select File then New Project (We are using Expression Blend because it has templates to easily create View Model style controls) all of this code will of course work with Visual Studio (I actually use Visual Studio to write all the code orginally).
Create a new Silverlight Application called InjectedApplication1
In the Projects window, right-click on References and select Add Reference...
Navigate to:
..\Program Files\Microsoft SDKs\Silverlight\v4.0\Libraries\Client\
Add references to:
- System.ComponentModel.Composition.dll
- System.ComponentModel.Composition.Initialization.dll
(you need these references, so we can add MEF attributes to the class, so the application can be recognized by MEF)
Additional Notes:
- You don't need System.ComponentModel.Composition.Initialization.dll to be referenced by your Silverlight Applications (part assemblies) unless they are using CompositionInitializer.
- For part assemblies you should set the MEF references (and other shared references) to be CopyLocal = false.
- CopyLocal = false will keep the size of the XAPs down, it will also prevent duplicates
Delete the MainPage.xaml page.
Select File then New Item...
Create a UserControl with ViewModel called MainPage.xaml.
Open the MainPageModel.cs file.
Replace ALL the code with the following code:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Data;
using System.ComponentModel;
namespace InjectedApplication1
{
public class MainPageModel : INotifyPropertyChanged
{
public MainPageModel()
{
Hello = "Hello From InjectedApplication1";
}
#region Hello
private String _Hello;
public String Hello
{
get { return _Hello; }
private set
{
if (Hello == value)
{
return;
}
_Hello = value;
this.NotifyPropertyChanged("Hello");
}
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
}
Open MainPage.xaml.cs.
Replace ALL the code with the following code:
using System.ComponentModel.Composition;
using System.Windows.Controls;
namespace InjectedApplication1
{
[Export(typeof(UserControl))]
public partial class MainPage : UserControl
{
public MainPage()
{
this.InitializeComponent();
}
}
}
(note we are adding the attribute "[Export(typeof(UserControl))]" to the class to allow it to be recognized by MEF)
Open MainPage.xaml.
Click on the UserControl in the Objects and Timeline window.
In the Properties, set the Layout Width and Height to 200.
Click the New button next to DataContext
Select MainPageModel and click OK.
- Click on the Data tab
- Under Data Context, click and drag Hello: (String) to the design surface
(note, if the Data Context is empty, build the project, close MainPage.xaml and re-open it)
The text will show.
From the Project tab, select Build Project.
In the file manager on your computer, when you look in:
...\InjectedApplication1\InjectedApplication1\Bin\Debug
You will see:
InjectedApplication1.xap
Later, you will drop it into the ClientBin directory of the Main Silverlight Application to be dynamically injected.
Repeat the process to create another Silverlight Application called InjectedApplication2.
(note: you can download the .xap's from this link: InjectedApplicationXAPs.zip)
You can also hit F5 to build and run the project. It is basically a normal Silverlight project with a few References, using statements, and an attribute added.
The Main Silverlight Application
We are now ready to create the Main Silverlight application that will use MEF to dynamically inject the two Silverlight Applications that we just created.
Create a new Silverlight Application + Website called MainApplication
Add references to:
- System.ComponentModel.Composition.dll
- System.ComponentModel.Composition.Initialization.dll
- System.Windows.Interactivity
(note: Get System.Windows.Interactivity.dll from ..\Program Files\Microsoft SDKs\Expression\Blend\Silverlight\v4.0\Libraries\)
(note: System.Windows.Interactivity.dll is used to make the behaviors (used later) work. It is not part of MEF)
Delete the MainPage.xaml page.
Create a UserControl with ViewModel called MainPage.xaml.
Add a new folder and call it HelperClasses.
ICommand Support
Add a class to the HelperClasses folder called DelegateCommand.cs.
Replace ALL the code with the following code:
using System.Windows.Input;
using System;
namespace MainApplication
{
public class DelegateCommand : ICommand
{
Func<object, bool> canExecute;
Action<object> executeAction;
bool canExecuteCache;
public DelegateCommand(Action<object> executeAction, Func<object, bool> canExecute)
{
this.executeAction = executeAction;
this.canExecute = canExecute;
}
#region ICommand Members
public bool CanExecute(object parameter)
{
bool temp = canExecute(parameter);
if (canExecuteCache != temp)
{
canExecuteCache = temp;
if (CanExecuteChanged != null)
{
CanExecuteChanged(this, new EventArgs());
}
}
return canExecuteCache;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
executeAction(parameter);
}
#endregion
}
}
This class allows us to easily invoke ICommands. You can get more information on this class at: http://johnpapa.net/silverlight/5-simple-steps-to-commanding-in-silverlight/
Deployment Catalog Service (COMPLICATED STUFF!)
We are now getting into the "complicated stuff". We are going to create a class that will actually load the .xap on demand. We will create an Interface for it's methods; AddXap and RemoveXap, and then we will implement the class (we are putting both in the same file to simply things, normally you would put your interface in it's own file).
Add a class to the HelperClasses folder called DeploymentCatalogService.cs.
Replace ALL the code with the following code:
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel;
namespace MainApplication
{
public interface IDeploymentCatalogService
{
void AddXap(string uri, Action<AsyncCompletedEventArgs> completedAction = null);
void RemoveXap(string uri);
}
[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 uri, Action<AsyncCompletedEventArgs> completedAction = null)
{
DeploymentCatalog catalog;
if (!_catalogs.TryGetValue(uri, out catalog))
{
catalog = new DeploymentCatalog(uri);
if (completedAction != null)
{
catalog.DownloadCompleted += (s, e) => completedAction(e);
}
else
{
catalog.DownloadCompleted += catalog_DownloadCompleted;
}
catalog.DownloadAsync();
_catalogs[uri] = catalog;
_aggregateCatalog.Catalogs.Add(catalog);
}
}
void catalog_DownloadCompleted(object sender, AsyncCompletedEventArgs e)
{
if (e.Error != null)
{
throw e.Error;
}
}
public void RemoveXap(string uri)
{
DeploymentCatalog catalog;
if (_catalogs.TryGetValue(uri, out catalog))
{
_aggregateCatalog.Catalogs.Remove(catalog);
_catalogs.Remove(uri);
}
}
}
}
For a full explanation on this class, see this article and this article.
Yes we could go into the details of "how" it does what it does, but that is covered in the links above. If you want to know "what" it does, it loads a requested .xap into a "catalog". The code we will cover next, pulls the .xap out of the catalog and loads it into the Main Silverlight Application.
Dynamically Downloading a Silverlight Application Using MEF
We will now create the View Model. It will basically perform the function in the diagram above.
Open up MainPageModel.cs and replace ALL the code with the following code:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Data;
using System.ComponentModel;
using System.Windows.Input;
using System.ComponentModel.Composition;
using System.Windows.Controls;
using System.Linq;
using System.Collections.ObjectModel;
namespace MainApplication
{
public class MainPageModel : INotifyPropertyChanged, IPartImportsSatisfiedNotification
{
private string strSelectedXAP = "";
public MainPageModel()
{
LoadXAPCommand = new DelegateCommand(LoadXAP, CanLoadXAP);
SetMEFPanelCommand = new DelegateCommand(SetMEFPanel, CanSetMEFPanel);
if (!DesignerProperties.IsInDesignTool)
{
CompositionInitializer.SatisfyImports(this);
}
}
#region Commanding
public ICommand LoadXAPCommand { get; set; }
public void LoadXAP(object param)
{
ComboBoxItem objComboBoxItem = (ComboBoxItem)param;
strSelectedXAP = objComboBoxItem.Content.ToString();
CatalogService.AddXap(strSelectedXAP);
LoadSelectedModule();
}
private bool CanLoadXAP(object param)
{
return true;
}
public ICommand SetMEFPanelCommand { get; set; }
public void SetMEFPanel(object param)
{
MefPanel = (Panel)param;
}
private bool CanSetMEFPanel(object param)
{
return ((param as Panel) != null);
}
#endregion
[Import]
public IDeploymentCatalogService CatalogService { get; set; }
[ImportMany(AllowRecomposition = true)]
public Lazy<UserControl>[] MEFModuleList { get; set; }
#region IPartImportsSatisfiedNotification Members
public void OnImportsSatisfied()
{
LoadSelectedModule();
}
#endregion
#region LoadSelectedModule
private void LoadSelectedModule()
{
if (MefPanel != null)
{
string strRevisedSelectedXAPName = strSelectedXAP.Replace(".xap", ".");
var SelectedMEFModuleInPanel = (from Module in MefPanel.Children.Cast<UserControl>()
where Module.ToString().Contains(strRevisedSelectedXAPName)
select Module).FirstOrDefault();
if (SelectedMEFModuleInPanel == null)
{
MefPanel.Children.Clear();
var SelectedMEFModule = (from Module in MEFModuleList.ToList()
where Module.Value.ToString().Contains(strRevisedSelectedXAPName)
select Module).FirstOrDefault();
if (SelectedMEFModule != null)
{
MefPanel.Children.Add(SelectedMEFModule.Value);
}
}
}
}
#endregion
#region MefPanel
private Panel _MefPanel;
public Panel MefPanel
{
get { return _MefPanel; }
private set
{
if (MefPanel == value)
{
return;
}
_MefPanel = value;
this.NotifyPropertyChanged("MefPanel");
}
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
}
The diagram above shows the class view of the View Model.
We have reached the apex of the "complicated stuff". It's all downhill from here. There are links at the end of this article that will fully explain how the code above works. The general explanation is in the comments in the code.
The View (The UI)
Open MainPage.xaml.
Switch to XAML View.
Replace ALL the code with the following code:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MainApplication"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
x:Class="MainApplication.MainPage"
UseLayoutRounding="True"
Width="640" Height="480">
<UserControl.Resources>
<local:MainPageModel x:Key="MainPageModelDataSource" />
</UserControl.Resources>
<Grid x:Name="LayoutRoot" DataContext="{Binding Source={StaticResource MainPageModelDataSource}}">
<TextBlock HorizontalAlignment="Left" Height="40" Margin="8,8,0,0" TextWrapping="Wrap" Text="Main Page"
VerticalAlignment="Top" Width="168" FontSize="32" Foreground="#FFC82929"/>
<Button Content="Load" HorizontalAlignment="Right" Height="24" Margin="0,24,144,0"
VerticalAlignment="Top" Width="96" />
<StackPanel x:Name="MEFContent" Margin="12,56,16,16"/>
<ComboBox x:Name="comboBox" VerticalAlignment="Top" Margin="192,28,256,0" SelectedIndex="0">
<ComboBoxItem Content="InjectedApplication1.xap"/>
<ComboBoxItem Content="InjectedApplication2.xap"/>
</ComboBox>
</Grid>
</UserControl>
Switch back to Design View and you will see the UI.
Wire It Up! - The Panel
We need to connect the View to the View Model by registering the StackPanel as the place to inject the Silverlight applications, and the Load button to load the Silverlight Applications.
Get an InvokeCommandAction Behavior.
Drop it on the LayoutRoot in the Objects and Timeline window.
In the Properties for the Behavior:
- Select Loaded for EventName (this means the behavior will run when the application starts)
- Click the Data bind icon next to Command (under Common Properties)
Select SetMEFPanelCommand and click OK.
Select Advanced options for CommandParameter (we now need to indicate exactly what Panel element we want the Silverlight Applications to be placed inside of)
Select Data Binding...
Select the MEFContent StackPanel (on the Element Property tab) and click OK.
Wire It Up! - The Load Button
Drop an InvokeCommandAction Behavior on the [Button]
In the Properties for the Behavior:
- Select Click for EventName (the Behavior will ruin when the Load button is clicked)
- Click the Data bind icon next to Command (under Common Properties)
Bind to LoadXAPCommand
Select Advanced options for CommandParameter and then select Element Property Binding...
Click on the comboBox in the Objects and Timeline window
Select SelectedItem in the Property of comboBox dropdown and click OK.
The diagram above shows what is now wired to what.
One More Thing - Initialize the DeploymentCatalogService
Open the App.xaml.cs file.
Change the Application_Startup method to:
private void Application_Startup(object sender, StartupEventArgs e)
{
DeploymentCatalogService.Initialize();
this.RootVisual = new MainPage();
}
We need to add the code "DeploymentCatalogService.Initialize()" to initialize the DeploymentCatalogService, for it to work.
Add the Silverlight Applications
When you look in the website project in the Projects window, you will only see the MainApplication.xap file in the ClientBin directory.
Drag and drop the InjectedApplication1.xap and InjectedApplication2.xap files into the directory.
Hit F5 to build and run the application.
You will be able to dynamically load each application.
Why Should You Use MEF To Dynamically Load Silverlight Applications ?
There are other ways to dynamically load a Silverlight .xap. I have a project (Introducing SilverlightDesktop.net) that does that. the problem I ran into, was handling the project dependencies. For example, if you use a control from the Silverlight Control Toolkit, it wont work. MEF will resolve any dependencies. This is really important when using View Model Style. A Designer could add a lot of custom Behaviors and other dependencies.
This is an example where using MEF is LESS code than if you used an alternative method (that provided the same powerful functionality). The hard part with MEF is putting the correct attributes in the correct places and setting their values properly. Hopefully this tutorial provided a helpful example.
There Is More To MEF
There is so much more to MEF, that this simple example is almost insulting to the technology. However, I fully expect Microsoft to create more tooling and example code to make it's implementation of it's other features easier in the future.
A special thanks to Glenn Block for help with this article.
Further Reading