An Introduction to the Managed Extensibility Framework
[Note: This article was based upon a very early preview of the Managed Extensibility Framework and may differ substantially from later releases.]
Introduction
Over the years applications have followed an increasing trend of providing the ability to extend their core features through the use of various extensibility mechanisms. From the Microsoft Office suite of applications, to Internet browsers, to instance messenger programs … virtually every application we use today offers some form of extensibility. Unfortunately, developing extensible applications on the .Net platform has always required development teams to provide their own infrastructure. That is, until now.
The Managed Extensibility Framework is a new library being developed by Microsoft to support the creation of extensible applications on the .Net platform. The following is a brief introduction to the features within the Managed Extensibility Framework.
Overview
The Managed Extensibility Framework provides an infrastructure for enabling an application to easily consume extensions by providing the ability to dynamically bind internal and add-in components together through contract-based declarations. The main types of components within the framework are the Container, the Catalog, and the composable Parts.
Container
The Container is responsible for managing the creation and composition of dependencies between parts. It is represented within the framework by the type CompositionContainer. This type implements an interface, ICompositionService, which can itself be added to the container for explicit use by Parts during the lifetime of the application.
Catalogs
Catalogs are responsible for discovering dependencies between the registered Parts. All catalogs extend the abstract base type ComposablePartCatalog. There are currently four types of catalogs provided within MEF:
AttributedAssemblyPartCatalog | provides discovery of Parts within a collection of types |
AttributedTypesPartCatalog | provides discovery of Parts within a given assembly |
DirectoryPartCatalog | provides discovery of Parts within a given file system directory |
AggregatingComposablePartCatalog | provides the ability to combine multiple catalogs into a single composite catalog |
Parts
Parts are the logical units of composition within MEF and are used to encapsulate information about types used during the composition process. Each Part is identified by a Contract and contains a list of Exports and Imports. Contracts take the form of a string identifier and are used to identify dependencies between Parts. Exports specify the services offered to other Parts, while Imports specify services required by other Parts.
Example Application – MefPad
The following example demonstrates the basic features of the Managed Extensibility Framework by walking through the creation of a simple text editor application which utilizes extensions as the file persistence mechanism.
Step 1 – Project and Form Creation
- Create a new Windows Forms Application project named “MefPad”.
- Add a reference to the System.ComponentModel.Composition assembly.
- Rename “Form1.cs” to “MefPad.cs”.
- Add a MenuStrip control docked at the top.
- Add a top level menu item named “File”.
- Add a submenu item under “File” named “Open”.
- Double-click on the “Open” menu item to auto-create a Click event handler.
- Return to the designer and add another submenu item under “File” named “Save”.
- Double-click on the “Save” menu item to auto-create a Click event handler.
- Return to the designer and add a TextBox control named “mainContentTextBox”.
- Change the Dock property to “Fill”.
The following illustrates the resulting MefPad form design:
Step 2 – Configure MEF Container
- Add the following call to Compose() in the MefPad constructor:
public MefPad() { InitializeComponent(); Compose(); }
- Add the following method:
void Compose() { // search currently directory for extension assemblies var catalog = new DirectoryPartCatalog(".", "*Extensions.dll", true); var container = new CompositionContainer(catalog); container.AddPart(this); container.Compose(); }
This method uses a DirectoryPartCatalog to discover any MefPad extensions. In this case, the catalog will discover any assemblies in the current executing directory whose name ends with “Extensions.dll”. Next, a CompositionContainer is instantiated with the catalog. The MefPad Form (i.e. “this”) is then added as a Part to the container. This will allow any Exports or Imports within the MefPad class to be discovered by the container. Finally, the container is directed to compose all Parts.
Step 3 – Define an Extension
- Create a new interface type named IFilePersistenceExtension:
using System.IO; namespace MEFPad { public interface IFilePersistenceExtension { /// <summary> /// Description of the file type. /// </summary> string Description { get; } /// <summary> /// The extension of the file type. /// </summary> string Extension { get; } /// <summary> /// Reads from an open stream. /// </summary> /// <param name="stream">An open <see cref="Stream"/></param> /// <returns>character array of text read.</returns> char[] Read(Stream stream); /// <summary> /// Writes to an open stream. /// </summary> /// <param name="stream">An open <see cref="Stream"/></param> /// <param name="text">The text to be written to the stream</param> void Write(Stream stream, char[] text); } }
This interface defines the types which MefPad will use for reading and saving files to disk. The Description and Extension properties will be used by the file dialog box presented to the user while the Read() and Write() methods will be used to open and save files respectively.
Step 4 – Create an Import
- Add the following property to the MefPad class:
[Import] public List<IFilePersistenceExtension> FilePersistenceExtensions { get; set; }
This property will contain the collection of IFilePersistenceExtension types which will be presented as file type options to the user. The [Import] attribute is used to indicate that the MefPad Part has a dependency upon IFilePersistenceExtension types. Upon discovering any Parts with a contract of this type within the Catalog, the Container will add instances of the Part to the FilePersistenceExtensions collection.
Step 5 – Create the Business Logic
- Add the following method to the MefPad.cs class:
T GetFileDialog() where T : FileDialog, new() { var fileDialog = new T(); var fileTypesBuffer = new StringBuilder(); if (FilePersistenceExtensions != null) FilePersistenceExtensions .ForEach(x => fileTypesBuffer.Append(string.Format("{0}{1}|*{2}", (fileTypesBuffer.Length == 0) ? null : "|", x.Description, x.Extension))); fileDialog.Filter = fileTypesBuffer.ToString(); return fileDialog; } }
This method provides the common setup behavior used when configuring the open and save dialog boxes to be presented to the user. The ForEach() method is used to add file types to the dialog filter.
- Replace the openToolStripMenuItem_Click() and saveToolStripMenuItem_Click() event handler methods with the following code:
void openToolStripMenuItem_Click(object sender, EventArgs e) { Stream stream; OpenFileDialog openFileDialog = GetFileDialog<OpenFileDialog>(); if (openFileDialog.ShowDialog() == DialogResult.OK) { if ((stream = openFileDialog.OpenFile()) != null) { char[] content = FilePersistenceExtensions[openFileDialog.FilterIndex - 1].Read(stream); mainContentTextBox.Text = new String(content); stream.Close(); } } } void saveToolStripMenuItem_Click(object sender, EventArgs e) { Stream stream; SaveFileDialog saveFileDialog = GetFileDialog<SaveFileDialog>(); if (saveFileDialog.ShowDialog() == DialogResult.OK) { if ((stream = saveFileDialog.OpenFile()) != null) { FilePersistenceExtensions[saveFileDialog.FilterIndex - 1].Write(stream, mainContentTextBox.Text.ToCharArray()); stream.Close(); } } }
These methods handle the “Open” and “Save” Click menu item events respectively. The openToolStripMenuItem_Click() method calls the Read() method of the IFilePersistenceExtension instance corresponding to the selected filter index. Likewise, the saveToolSripMenuItem_Click() method calls the Write() method of the IFilePersistenceExtension instance corresponding to the selected filter index.
Step 6 – Create the Extensions
- Create a new Class Library project named MefPadExtensions.
- Add a reference to the MefPad project.
- Add a reference to the System.ComponentModel.Composition.
- Change the build output folder to ..MEFPadbinDebug.
- Create a new class named TextFilePersistenceExtension with the following source:
using System.IO; namespace MEFPadExtensions { [Export(typeof (IFilePersistenceExtension))] public class TextFileSaverExtension : IFilePersistenceExtension { public string Description { get { return "Text file"; } } public string Extension { get { return ".txt"; } } public char[] Read(Stream stream) { var streamReader = new StreamReader(stream); string bytes; try { bytes = streamReader.ReadToEnd(); return bytes.ToCharArray(); } finally { streamReader.Close(); stream.Close(); } } public void Write(Stream stream, char[] text) { var streamWriter = new StreamWriter(stream); try { streamWriter.Write(text); } finally { streamWriter.Close(); stream.Close(); } } } }
This class provides the MefPad application with simple file persistence capabilities.
- Create a new class named EncryptedFilePersistenceExtension with the following source:
using System.IO; using System.Security.Cryptography; using System.Text; namespace MEFPadExtensions { [Export(typeof (IFilePersistenceExtension))] public class EncryptedFilePersistenceExtension : IFilePersistenceExtension { readonly byte[] iv = Encoding.ASCII.GetBytes("ABCDEFGHIJKLMNOP"); readonly byte[] key = Encoding.ASCII.GetBytes("ABCDEFGHIJKLMNOP"); public string Description { get { return "Encrypted"; } } public string Extension { get { return ".crypt"; } } public char[] Read(Stream stream) { Rijndael rijndael = Rijndael.Create(); var cryptoStream = new CryptoStream(stream, rijndael.CreateDecryptor(key, iv), CryptoStreamMode.Read); var streamReader = new StreamReader(cryptoStream); string bytes; try { bytes = streamReader.ReadToEnd(); return bytes.ToCharArray(); } finally { streamReader.Close(); cryptoStream.Close(); stream.Close(); } } public void Write(Stream stream, char[] text) { Rijndael rijndael = Rijndael.Create(); var cryptoStream = new CryptoStream(stream, rijndael.CreateEncryptor(key, iv), CryptoStreamMode.Write); var streamWriter = new StreamWriter(cryptoStream); try { streamWriter.Write(text); } finally { streamWriter.Close(); cryptoStream.Close(); stream.Close(); } } } }
This class provides the MefPad application with encrypted file persistence capabilities.
Step 7 – Run the Application
- Start the application and enter some text into the text box.
- From the menu bar, select File -> Save
Notice in the Save as type: dropdown box that two choices are presented: “Text file” and “Encrypted”. Selecting each of these types will result in the entered text being saved using the corresponding extension. The source for this example can be downloaded here.
Conclusion
The Managed Extensibility Framework provides developers with the tools needed to easily create extensible .Net applications. Moreover, its future ubiquitous presence as an option for enabling extensibility will provide a readily accessible solution across projects and companies. By leveraging MEF for your extensibility needs, you’ll be able to focus less on the concerns of infrastructure development and more on simply creating great software for your company.