Ask Kathleen

Working with MEF

Learn how to free your application from dependencies and interchange implementations using Managed Extensibility Framework.

Q: I've been hearing the term MEF lately and I know it means Managed Extensibility Framework, but I don't understand what it does. Would it be a good fit for allowing customers to add their own forms to our Windows Presentation Foundation (WPF) app? We need those new forms to appear in our menus and we don't want to give customers our Visual Basic source code to recompile.

A: This sounds like a good application for MEF. You could use the same approach for WinForms or ASP.NET and the C# code would be similar to what I'll show here.

MEF is an extensibility model that allows components to interact using a simple model. Components are called parts in MEF (see Figure 1). MEF is designed to be extensible to different types of models, but ships with an attributed model. This lets you define the interaction between your parts via normal .NET attributes. If a part needs something, it uses an Import attribute and if a part supplies something it uses an Export attribute. Any part can be a host or client, provider or consumer. MEF bases these interactions on contracts and offers a flexible discovery model. Your application makes a request and leaves it to MEF to provide the implementation. This frees your application from dependencies and lets you interchange implementations.

MEF is currently available as a preview on CodePlex. It's in the System.ComponentModel namespace, indicating that it will appear as a full member of .NET Framework 4.0. Several projects within Microsoft are committed to it, including the Visual Studio (VS) 2010 code editor. Until it appears in .NET Framework, you'll need to download MEF and place System.ComponentModel.Composition.dll in an accessible location. You should also realize that changes to the API may occur.

Your application will be the MEF host while your customers will write MEF clients or extensions. Extensions are simpler to write because the host must also manage the CompositionContainer. I'll step through the process of creating an interface, building extensions and building a WPF host. The WPF host will pass a catalog to a CompositionContainer that will specify a directory to search for menu extensions.

Extensions support contracts. The most common contracts are interfaces, and these interfaces might be quite simple:

Public Interface IExtension
   Sub ShowWindow()
End Interface 

Your customers can add extensions by implementing this interface and specifying the Export attribute from the System.ComponentModel.Composition namespace:

<Export(GetType(IExtension))> _
<ExportMetadata("MenuCaption", "First")> _
Partial Public Class First
   Implements IExtension

   Public Sub ShowWindow() _
      Implements Common.IExtension.ShowWindow
   End Sub
End Class

You can also use an arbitrary string to identify the export, but the common case for extensions is to retrieve via an interface contract. The ExportMetadata attribute lets you include extra information about the extension that the host can retrieve without actually instantiating the underlying object, which lets you control instantiation and protects performance.

When you design your solution, put interfaces in a separate assembly referenced by both the host and all extension assemblies. Do not establish any direct references between your host and extensions. All of your assemblies will need a reference to System.ComponentModel.Composition.dll. Your compiled extensions will need to be placed in a convenient location. You can specify the build location in the Project Properties dialog.

The host is responsible for managing the Compo­sitionContainer. The CompositionContainer hooks up imports and exports behind the scenes and must know what exports are available. Creating the container as a project level variable lets you clean it up in OnExit. The call to the container's Dispose method disposes of any parts that implement IDisposable:

Protected Overrides Sub OnExit( _
   ByVal e As System.Windows.ExitEventArgs)
   If mContainer IsNot Nothing Then
   End If
End Sub

Overriding the OnStartup method let's you prepare the container when your application starts:

Protected Overrides Sub OnStartup( _
   ByVal e As StartupEventArgs)
   If Compose() Then
   End If
End Sub

The compose method does the actual preparation:

Private Function Compose() As Boolean
   Dim cat As New AggregateCatalog

The aggregate catalog allows you to manage several catalogs together. If a set of catalogs appear together in an aggregate catalog, all matching items within this set of catalogs are discovered. Several different types of catalogs are available, including DirectoryCatalog, which loads all assemblies in a specified directory. In your case, I'd suggest including the location of extensions as part of the application settings to allow later configuration. You'll also add the current assembly:

Dim extLocation = My.Settings.ExtensionLocation
cat.Catalogs.Add(New DirectoryCatalog(extLocation))
cat.Catalogs.Add(New AssemblyCatalog( _
mContainer = New CompositionContainer(cat)

At this point the container is ready to work, but no work has been requested. A composition batch allows you to specify the items to evaluate for Import requests when you call the Compose method. Any object later instantiated via an Import request will also be evaluated for additional Import requests. In many cases the first call to Compose will be the only one you need to perform. Because Compose performs the composition, it will fail if composition rules are not followed, so the Try/Catch block provides reporting:

   Dim batch = New Hosting.CompositionBatch()
   Catch ex As CompositionException
      Return False
   End Try
   Return True
End Function

The Main window of the application uses an Import attribute on a field. For MEF to satisfy this request and create the field value, the Main window itself must be provided via MEF -- MEF evaluates Import attributes on anything explicitly added to a batch and anything that is instantiated via another Import. You can do this by replacing the MainWindow method in the base class via Overloads (new in C#):

<Import(GetType(Main))> _
Public Overloads Property MainWindow() As Window
   Return MyBase.MainWindow
  End Get
  Set(ByVal value As Window)
   MyBase.MainWindow = value
  End Set
End Property

There's one more detail specific to VB. The Application Framework automatically instantiates the specified window and bypasses your MainWindow property. Disable the Application Framework and add a Sub Main to the Application.xaml.vb file:

'''Application Entry Point.
<System.STAThreadAttribute()> _
Public Shared Sub Main()
  Dim app As Application = New Application
End Sub

This causes the application to retrieve the MainWindow via the property and thus evaluates MEF requests in the Main window. The Main window exports itself to MEF to match the MainWindow property import:

<Export(GetType(Main))> _
Partial Public Class Main

The Main window uses MEF to discover available extensions that implement the IExtension interface:

<Import(GetType(IExtension))> _
Private exportExtensions As ExportCollection( _
   Of IExtension)

Anything that appears in the catalog or is manually added to the container through batches and implements the IExtension interface will appear in this collection. Note that this creates a collection of Export objects that contain sufficient information to instantiate the actual extensions, but they do not yet instantiate them. This is important to maintain adequate performance.

The Main window fills the menu using standard WPF code:

Private Sub Main_Loaded() Handles Me.Loaded
  For Each export In exportExtensions
     Dim newItem = New MenuItem()
     newItem.Header = export.Metadata("MenuCaption")
End Sub

Note that the menu caption is retrieved from the MEF metadata. While this works, your goal is to make life as easy as possible for programmers writing extensions. This metadata approach requires they know to use the exact string "MenuCaption". It's easy to fix this using strongly typed MEF metadata. To begin, create an interface:

Public Interface IExtensionMetadata
 ReadOnly Property MenuCaption() As String
End Interface

Now create an attribute that parallels this interface. This attribute needs to match the metadata interface. It does not need to implement the interface, but this is the easiest way to keep them in sync:

<MetadataAttribute(), AttributeUsage( _
   AttributeTargets.Class)> _
Public Class ExtensionMetadataAttribute
   Inherits Attribute
   Implements IExtensionMetadata
   Private mMenuCaption As String
   Public Sub New(ByVal menuCaption As String)
      mMenuCaption = menuCaption
   End Sub
   Public ReadOnly Property MenuCaption() _
      As String Implements IExtensionMetadata.MenuCaption
         Return mMenuCaption
      End Get
   End Property
End Class

Your customers can decorate their extensions with this attribute:

<Export(GetType(IExtension))> _
<ExtensionMetadata("First")> _
Partial Public Class First

You can leverage this metadata by altering the Import request in the Main window to include a second type parameter:

<Import(GetType(IExtension))> _
Private exportExtensions As ExportCollection( _
   Of IExtension, IExtensionMetadata)

This provides a strongly typed MetadataView to simplify access to export metadata:

newItem.Header = export.MetadataView.MenuCaption

While I've answered your question, I don't think I've solved your problem yet. At this point, the windows display but do not interact with each other or the rest of your application. That next step is easy because MEF makes no distinction between extensions and host while resolving Import and Export attributes. The sample in the download uses additional interfaces to provide a string to extensions from the host as a simple demonstration. Your app will probably provide more sophisticated functionality such as the parent window, where extension user controls or specific business objects or application data should be sited.

It's valuable to export classes, not primitive values or structures. The values of reference types will reflect changes as your application proceeds rather than reflecting only the value MEF supplied at composition. This also means two-way communications can be provided either through mutable objects that allow changes, or through immutable objects that expose specific data. In addition to host/extension interactions, multiple extensions can communicate with each other using interfaces that are entirely unknown to the host or initial programmers.

The compiler shortcuts recompiling non-referenced assemblies and a composable application doesn't maintain references. Avoid problems with out-of-date assemblies by using Rebuild Solution before testing your application.

MEF is similar to System.AddIn (also called MAF), which I discussed in April 2008 (see "Extend Your Apps with External Add-Ins"). MAF is significantly more complex to use, but solves additional problems of isolation and versioning. MEF extensions run in the AppDomain of the host, with the rights of your AppDomain. This means when using MEF you must trust extensions not to run malicious code, or offer protection via Code Access Security. MAF solves this problem by creating extensions in a separate AppDomain, which you can lock down or sandbox.

You are committing to maintaining the interfaces you release to your customers. If you alter the interface, you will break their code. Instead you can create a new interface, leaving the previous interface intact. MEF itself is extensible. If you encounter versioning or AppDomain isolation problems, you could create an additional programming model that would combine the MEF and MAF, however you would reintroduce some of the complexities of MAF.

MEF also has a direct access API, which you might require, but the Import/Export attribute model is desirable for most scenarios in VB or C#. The full extensibility of MEF allows its use in other scenarios, including dynamic languages, external definitions and alternate discovery mechanisms.

So far, I've skipped over the important issues of cardinality and lifetime. When you place the Import attribute on a single item, you state that you expect a single item. The standard MEF container configuration throws an exception if no matches or multiple matches are found:

<Import(GetType(ITextToDisplay))> _
Private textToDisplay As ITextToDisplay

If you know there may be multiple matches, you can import into an IEnumerable or an ExportCollection. If you don't want an exception thrown if no match is found, you can set the AllowDefault parameter on the Import attribute to True. If you don't know how many matches will be discovered, place the import on a collection and manage getting the correct instance in your code.

Lifetime refers to whether a single instance of the export is used for all requests (CreationPolicy.Shared), or a new instance created for each request (CreationPolicy.NonShared). You declare the lifetime using a CreationPolicy parameter on the Export attribute or the RequiredCreationPolicy on the Import attribute. The default creation policy is Any and if both Export and Import attributes have a CreationPolicy of Any, the result is a shared instance. If the CreationPolicies conflict, MEF throws an exception. This allows you fine grain and flexible control over instance creation from either the export or import side.

MEF has the capacity to let you build highly decoupled applications that support customer extensions, granular development, test mocking and good programming design. It's a good technique to add to your arsenal.

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.