What are some best practices for using MEF in your code? Are there any pitfalls to take into account when starting your extensible application? Did you run into anything you should have known earlier?
the best-practice is to use the Shared (singleton) model. This bring us to design consideration which suggest that you should design your exportable parts as stateless and thread safe, so they won't be affected by multiple calls (maybe on different threads) upon the same instance.
In cases that the singleton model is not suitable for you, it is recommended to use builder pattern (to separate the exported part from the actual instantiation). You should remember that using the non shared model is quite costly because it is using reflection for the actual instantiation (by using the builder pattern you can gain the same result with less pain).
Also look here http://blogs.microsoft.co.il/blogs/bnaya/archive/2010/01/09/mef-for-beginner-toc.aspx And of cource you know that you can find info here: http://mef.codeplex.com
I'm in the midst of building a fully fledged extensible application on MEF (and using WPF with the MVVM pattern). I took the basic application framework that I built and open sourced it as SoapBox Core. I also published a demo based on SoapBox Core over at Code Project: Building an Extensible Application with MEF, WPF, and MVVM.
I'm not sure if using MVVM applies to you, but if it does, then there's a lot you might learn by looking at the implementation of MVVM with MEF. Particularly the way it imports Views.
As far as best practices... I created a nested hierarchy of extensions (so the basic module is called Host, and all it does is compose the application and import a few basic extensions). Then those extensions expose other extension points and the application kind of builds itself when you run it (a cross between composition and extensions).
To keep everything straight, I put the extension hierarchy into a set of static classes. For instance, here are all the extension points that the core framework provides:
namespace SoapBox.Core.ExtensionPoints
{
public static class Host
{
public const string Styles = "ExtensionPoints.Host.Styles";
public const string Views = "ExtensionPoints.Host.Views";
public const string StartupCommands = "ExtensionPoints.Host.StartupCommands";
public const string ShutdownCommands = "ExtensionPoints.Host.ShutdownCommands";
}
public static class Workbench
{
public const string ToolBars = "ExtensionPoints.Workbench.ToolBars";
public const string StatusBar = "ExtensionPoints.Workbench.StatusBar";
public const string Pads = "ExtensionPoints.Workbench.Pads";
public const string Documents = "ExtensionPoints.Workbench.Documents";
public static class MainMenu
{
public const string Self = "ExtensionPoints.Workbench.MainMenu";
public const string FileMenu = "ExtensionPoints.Workbench.MainMenu.FileMenu";
public const string EditMenu = "ExtensionPoints.Workbench.MainMenu.EditMenu";
public const string ViewMenu = "ExtensionPoints.Workbench.MainMenu.ViewMenu";
public const string ToolsMenu = "ExtensionPoints.Workbench.MainMenu.ToolsMenu";
public const string WindowMenu = "ExtensionPoints.Workbench.MainMenu.WindowMenu";
public const string HelpMenu = "ExtensionPoints.Workbench.MainMenu.HelpMenu";
}
}
public static class Options
{
public static class OptionsDialog
{
public const string OptionsItems = "ExtensionPoints.Options.OptionsDialog.OptionsItems";
}
}
}
So if you wanted your extension to add something to the file menu, you would export something that implements IMenuItem with contract name SoapBox.Core.ExtensionPoints.Workbench.MainMenu.FileMenu
Each extension has an "ID" which is just a string identifier. These existing IDs are defined in another hierarchy:
namespace SoapBox.Core.Extensions
{
public static class Workbench
{
public static class MainMenu
{
public const string File = "File";
public const string Edit = "Edit";
public const string View = "View";
public const string Tools = "Tools";
public const string Window = "Window";
public const string Help = "Help";
public static class FileMenu
{
public const string Exit = "Exit";
}
public static class ViewMenu
{
public const string ToolBars = "ToolBars";
}
public static class ToolsMenu
{
public const string Options = "Options";
}
}
}
}
As you can see FileMenu already contains an Exit extension (that is pre-programmed to close the app). If you wanted to add an extension to the File menu you probably want it to appear before the Exit menu item. IMenuItem inherits from IExtension, which has two properties:
- InsertRelativeToID
- BeforeOrAfter
So your extension would return SoapBox.Core.Extensions.Workbench.MainMenu.FileMenu.Exit for InsertRelativeToID, and would return Before for the BeforeOrAfter property (an enumeration). When the workbench imports all the File menu extensions, it sorts everything based on these IDs. In this way, the later extensions insert themselves relative to the existing extensions.
I'm very new to MEF still, but I wanted to add more to this discussion because I continually go through hell when trying to figure out why things don't work the way I expect them to.
First of all, when you're working with MEF, I recommend adding System.ComponentModel.Composition to your solution, rather than just adding references to the assemblies. Although debugging issues in MEF feels like a recursive nightmare, it's absolutely inevitable and vital when you can't figure out what's going wrong.
This leads me to my next point, which is to never, ever forget that MEF doesn't know about what you don't tell it, or if you don't tell it properly. For example, my alpha application was working great with MEF -- I had it compose parts in the main GUI, and all of the assemblies that got loaded by the container (which were dependencies of the main app) exported the necessary interfaces. Things worked out well and I was able to get MEF to resolve instances when and where I wanted it to.
However, I just started working on the next version; some plugins loaded (those that exported an interface, but had no import requirements), while others didn't (those that did need imports). This time I composed parts in my ApplianceManager class, which is in charge of loading plugins, but the plugins needed to resolve imports from other classes in the application (in my case, the Model). I figured this should automatically happen, especially since I can see in the Catalog construction that those assemblies were detected... but I still can't get it to work... which brings me back to my first point -- add the source code, and not the assemblies alone. Debugging this is nearly driving me insane, but I'll eventually figure it out after a lot of diligent stepping through MEF code. :)
I'd like to see someone post an answer to this question that talks about architectures that facilitate easy MEF integration. The answer regarding toolbar menus and such is really good, but I would like to see something that talks about things that reside entirely on the Model side of MVVM. Like how plugin managers, databases, plugins, and shared libraries should be arranged. I am still trying to figure out why I had a reasonably okay time getting my first MEF app to work, but after getting more "experience" with it, I cannot get my new app to work 100%.
UPDATE 2010-06-09
I'd like to add another possible practice for helping you navigate through the MEF shrubbery. Today, I needed to do a sanity check on that project that I "just couldn't figure out", so I whipped up a simple, unconventional UML class diagram where I used dependencies to mark Imports and Exports. Here's what I found, which made the issue very clear.
Working application:
Non-working application:
Isn't that stupid? The Model wasn't loaded, and it was on an island all by itself. I believe this is why my MEF-based dependencies aren't getting resolved (if anyone can corret me if I am wrong here, I would appreciate it, though!)