Plugins in WPF MvvM with MEF

By Mirek on (tags: MEF, mvvm, Plugins, WPF, categories: architecture, code)

In this post I will show you the way we can implement a plugin functionality in WPF MvvM application with use of Managed Extensibility Framework (MEF).

MEF is a tool that may facilitates extending and decoupling your application. In some scenarios it is very similar to Dependency Injection, but is rather focused on one primary purpose: Import and Export of objects. What is good it is built in .Net Framework since version 4.0, so we need no extra libraries or packages to start working with MEF.

There is a lot information on the web regarding MEF, for instance From Zero to Proficient with MEF, an excellent post for those who start with MEF or this tutorial. Thus I will limit the content of this post to absolute minimum, providing you a solution for implementing plugins in WPF MvvM application.

Let’s create a simple WPF application with a button and content control on main window

   1: <Window x:Class="WPF_MEF_App.MainWindow"
   2:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:         xmlns:local="clr-namespace:WPF_MEF_App"
   5:         Title="MEF in WPF Test App"
   6:         Width="525"
   7:         Height="350">
   8:     <Window.DataContext>
   9:         <local:MainWindowModel />
  10:     </Window.DataContext>
  11:     <Grid>
  12:         <Grid.RowDefinitions>
  13:             <RowDefinition Height="45" />
  14:             <RowDefinition Height="*" />
  15:         </Grid.RowDefinitions>
  16:  
  17:         <Button Width="160"
  18:                 HorizontalAlignment="Center"
  19:                 VerticalAlignment="Center"
  20:                 Command="{Binding ImportPluginCommand}"
  21:                 Content="Import and embedd plugin" />
  22:  
  23:         <Border Grid.Row="1"
  24:                 Margin="10"
  25:                 HorizontalAlignment="Stretch"
  26:                 VerticalAlignment="Stretch"
  27:                 BorderBrush="Black"
  28:                 BorderThickness="1">
  29:             <ContentControl Content="{Binding PluginView}" />
  30:         </Border>
  31:     </Grid>
  32: </Window>

The purpose of ImportPluginCommand command is to import the plugin view and assign it to PluginView property to be displayed in content control. The goal is to have a different views (UserControls for the record), each in separate dll, be able to be import as a plugin and embed in our main application.
MainWindowModel looks like this

   1: using InternalShared;
   2: using System.ComponentModel.Composition;
   3: using System.ComponentModel.Composition.Hosting;
   4: using System.IO;
   5: using System.Reflection;
   6: using System.Windows.Input;
   7:  
   8: public class MainWindowModel : NotifyModelBase
   9: {
  10:    public ICommand ImportPluginCommand { get; protected set; }
  11:  
  12:    private IView PluginViewVar;
  13:    [Import(typeof(IView))]
  14:    public IView PluginView
  15:    {
  16:        get { return PluginViewVar; }
  17:        set
  18:        {
  19:            PluginViewVar = value;
  20:            NotifyChangedThis();
  21:        }
  22:    }
  23:  
  24:    private AggregateCatalog catalog;
  25:    private CompositionContainer container;
  26:    
  27:  
  28:    public MainWindowModel()
  29:    {
  30:        ImportPluginCommand = new DelegateCommand(ImportPluginExecute);
  31:        catalog = new AggregateCatalog();
  32:        string pluginsPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
  33:        catalog.Catalogs.Add(new DirectoryCatalog(pluginsPath, "Plugin*.dll"));
  34:        pluginsPath = Path.Combine(pluginsPath, "plugins");
  35:        if (!Directory.Exists(pluginsPath))
  36:            Directory.CreateDirectory(pluginsPath);
  37:        catalog.Catalogs.Add(new DirectoryCatalog(pluginsPath, "Plugin*.dll"));
  38:        container = new CompositionContainer(catalog);
  39:    }
  40:  
  41:    private void ImportPluginExecute()
  42:    {
  43:        container.ComposeParts(this);
  44:  
  45:    }
  46: }

And here the MEF comes in play. I decorated property PluginView with Import attribute which tells MEF that he must search exports of type IView and if one found, it must be instantiated and assigned to this property. If no export is found the exception will be thrown. To avoid that we can allow default value in above mentioned property by adding a parameter AllowDefault = true to the Import attribute. In MainWindowModel constructor we prepare the MEF import. First we must create a catalog where the MEF will be searching for exports. We have here three options. DirectoryCatalog allows to provide a relative or absolute path and a pattern for the assembly name. TypeCatalog to search all exports of specified types. In our sample application we use AggregateCatalog which can contain any number of other catalogs, and then add one DirectoryCatalog with executing assembly directory and one with “plugins” subdirectory. In our case we tell MEF to search assemblies in those two locations with assembly name matching pattern “Plugin*.dll”.

Finally in command method ImportPluginExecute() we trigger a MEF to search and import all required imports, compose them and assign in this instance.

Important thing we need to do is to add a reference to System.ComponentModel.Composition assembly to our project. Then we need to define a interface which the plugins will implement. For sake of simplicity I defined here just empty interfece IView

   1: public interface IView
   2: {
   3: }

Now we are ready to implement our plugin assembly.

Let’s add two new class library projects to our solution. One will be called InternalShared. It will contain our IView interface and as name suggest will be shared among all assemblies. Second will be our plugin assembly. Let it name be PluginCalculator. Then we need to add at least three references to our plugin project: InternalShared, System.ComponentModel.Composition and System.Xaml, since we will place a XAML control in this assembly.

Now we are ready to implement our plugin. So we add a WPF UserControl to PluginCalculatro project.

   1: <UserControl x:Class="PluginCalculator.CalculatorScreen"
   2:              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   5:              xmlns:local="clr-namespace:PluginCalculator"
   6:              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   7:              FontSize="15">
   8:     
   9:     
  10:     <Grid>
  11:         <Grid.ColumnDefinitions>
  12:             <ColumnDefinition Width="120" />
  13:             <ColumnDefinition Width="*" />
  14:         </Grid.ColumnDefinitions>
  15:         <Grid.RowDefinitions>
  16:             <RowDefinition Height="*" />
  17:             <RowDefinition Height="*" />
  18:             <RowDefinition Height="*" />
  19:         </Grid.RowDefinitions>
  20:  
  21:         <TextBlock HorizontalAlignment="Right"
  22:                    VerticalAlignment="Center"
  23:                    Text="Number one: " />
  24:         <TextBlock Grid.Row="1"
  25:                    HorizontalAlignment="Right"
  26:                    VerticalAlignment="Center"
  27:                    Text="Number two: " />
  28:         <TextBlock Grid.Row="2"
  29:                    HorizontalAlignment="Right"
  30:                    VerticalAlignment="Center"
  31:                    Text="Sum: " />
  32:  
  33:         <TextBox Grid.Row="0"
  34:                  Grid.Column="1"
  35:                  Height="25"
  36:                  Margin="10,0,10,0"
  37:                  VerticalAlignment="Center"
  38:                  Text="{Binding NumberOne,
  39:                                 UpdateSourceTrigger=PropertyChanged}" />
  40:         <TextBox Grid.Row="1"
  41:                  Grid.Column="1"
  42:                  Height="25"
  43:                  Margin="10,0,10,0"
  44:                  VerticalAlignment="Center"
  45:                  Text="{Binding NumberTwo,
  46:                                 UpdateSourceTrigger=PropertyChanged}" />
  47:         <TextBox Grid.Row="2"
  48:                  Grid.Column="1"
  49:                  Height="25"
  50:                  Margin="10,0,10,0"
  51:                  VerticalAlignment="Center"
  52:                  IsReadOnly="True"
  53:                  Text="{Binding Sum}" />
  54:  
  55:     </Grid>
  56: </UserControl>

This is simple, just three textboxes allowing to provide a numbers and displaying its sum. Next we add a class which will be the view model of our plugin control and implement it with required logic behind our control.

   1: public class CalculatorScreenModel : NotifyModelBase
   2: {
   3:     private double NumberOneVar;
   4:     public double NumberOne
   5:     {
   6:         get { return NumberOneVar; }
   7:         set
   8:         {
   9:             NumberOneVar = value;
  10:             Sum = NumberOne + NumberTwo;
  11:             NotifyChangedThis();
  12:         }
  13:     }
  14:     
  15:     private double NumberTwoVar;
  16:     public double NumberTwo
  17:     {
  18:         get { return NumberTwoVar; }
  19:         set
  20:         {
  21:             NumberTwoVar = value;
  22:             Sum = NumberOne + NumberTwo;            
  23:             NotifyChangedThis();
  24:         }
  25:     }
  26:     
  27:     private double SumVar;
  28:     public double Sum
  29:     {
  30:         get { return SumVar; }
  31:         set
  32:         {
  33:             SumVar = value;                
  34:             NotifyChangedThis();
  35:         }
  36:     }
  37:     
  38:     public ColorsScreenModel()
  39:     {
  40:     }
  41: }

and we set is as a DataContext of the control in Xaml

   1: <UserControl.DataContext>
   2:     <local:CalculatorScreenModel />
   3: </UserControl.DataContext>

Now the crucial part. We need do define what we want to export. Our main application will search for IView implementation as a exported object, so we need to decorate our control class properly

   1: using InternalShared;
   2: using System.ComponentModel.Composition;
   3: using System.Windows.Controls;
   4:  
   5: namespace PluginCalculator
   6: {
   7:     [Export(typeof(IView))]
   8:     public partial class CalculatorScreen : UserControl, IView
   9:     {
  10:         public CalculatorScreen()
  11:         {
  12:             InitializeComponent();
  13:         }
  14:     }
  15: }

The Export attribute is beain read by MEF and informs about what type of interface we export here.

And that is basically it. All we need now is to rebuild the solution, then copy the PluginCalculator.dll from its bin directory to our main application “plugins” directory. This is required since main application does not reference the plugin project (which is what we want) so the plugin assembly is not automatically copied to main application directory.

Here is what we should get after clicking on “Import and embed plugin” button.

MEFinWPF

What else we can do?

Well there is a lot of other options we did not cover in this post, but all of them are well described and available on the web.

The first thing we could do is to support more than one plugin at the same time. That can be easilly accomplished by adding new collection property to our MainWindowModel

   1: [ImportMany(typeof(IView), AllowRecomposition = true)]
   2: public IEnumerable<Lazy<IView>> Plugins;

With use of ImportMany attribute MEF will import all exported plugins of type IView and store them in this collection. Additionally we make here use of generic Lazy class, which MEF natively supports. This allows us to decide when plugin classes are instantiated instead of auto-instantiating them by MEF.

We could also add a metedata attributes to additionally describe our plugins, so we can for instance get the name of the plugin without creating its instance. But this is not in the scope of this post.

However I’ve put them in the solution source for this post which can be downloaded below.