Plugin Architecture using MEF Framework

Introduction

 
MEF is a managed extensibility framework. It's a light-weight framework to build a plugin solution. At the base level, MEF will use reflection. Before reading this article, I would recommend reading the following article, Simple Plugin Architecture Using Reflection. This article is about creating plugin architecture using reflection. In the current article, I am using the same example, but here we build a plugin solution with MEF framework.
 

Why MEF?

 
In normal reflection, we need to explicitly mention which type of object required so it will have explicit registration. With a complete assembly, we need to load in an application to find the required contract. However, in MEFm we have implicit parts with declarative syntax called “Import”/"ImportMany" and “Export”. The MEF composite engine will take the responsibility of satisfying its imports with its respective available’ s from other parts. 
 
The most important thing is MEF will find its import and export by its metadata. Here it will not load the assemblies. MEF will suggest creating Lazy objects for export types. In that case, MEF will not instantiate export types until the time comes for usage. MEF will allow reusability through the application.
 
We will take the example of building a calculator application for it.
 
Create a project of type Class Library and name it as Plugger. It will be responsible for connecting our main calculator application to the sub-libraries of the calculator.
 
In this project, we will add an interface with the name IPlugger, as shown below:
  1. using System.Windows.Controls;  
  2.   
  3. namespace MEF.Sample.Plugger.Contract  
  4. {  
  5.     public interface IPlugger  
  6.     {  
  7.         /// <summary>  
  8.         /// Name of plugger  
  9.         /// </summary>  
  10.         string PluggerName { getset; }  
  11.   
  12.         /// <summary>  
  13.         /// It will return UserControl which will display on Main application  
  14.         /// </summary>  
  15.         /// <returns></returns>  
  16.         UserControl GetPlugger();  
  17.     }  
  18. }  
Now we will create plug board where all plugs will connect to the main application. For this, create a WPF Project and below the code in MainWindow.xaml, add the below code:
  1. <Window x:Class="MEF.Sample.Calculator.MainWindow"  
  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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"  
  6.         xmlns:local="clr-namespace:MEF.Sample.Calculator"  
  7.         WindowStyle="None" ResizeMode="NoResize"  
  8.         mc:Ignorable="d" Height="450" Width="800" MouseDown="Window_MouseDown" BorderBrush="Silver" BorderThickness="1" >  
  9.     <Window.Resources>  
  10.         <Style TargetType="{x:Type TabItem}">  
  11.             <Setter Property="FontWeight" Value="Bold" />  
  12.             <Setter Property="Background" Value="White" />  
  13.             <Setter Property="Foreground" Value="Black" />  
  14.             <Setter Property="Template">  
  15.                 <Setter.Value>  
  16.                     <ControlTemplate TargetType="{x:Type TabItem}">  
  17.                         <Border BorderThickness="1" BorderBrush="LightGray" x:Name="TabBorder" Background="{TemplateBinding Background}"  Margin="3">  
  18.                             <ContentPresenter ContentSource="Header" Margin="3" />  
  19.                         </Border>  
  20.                         <ControlTemplate.Triggers>  
  21.                             <Trigger Property="IsSelected" Value="True">  
  22.                                 <Setter TargetName="TabBorder" Property="BorderBrush" Value="Purple" />  
  23.                             </Trigger>  
  24.                         </ControlTemplate.Triggers>  
  25.                     </ControlTemplate>  
  26.                 </Setter.Value>  
  27.             </Setter>  
  28.         </Style>  
  29.     </Window.Resources>  
  30.     <Grid>  
  31.         <Grid.RowDefinitions>  
  32.             <RowDefinition Height="30"/>  
  33.             <RowDefinition Height="400"/>  
  34.             <RowDefinition Height="20"/>  
  35.         </Grid.RowDefinitions>  
  36.         <Grid Grid.Row="0" Background="Orange">  
  37.             <Label Content="Calculator" Foreground="White" FontWeight="Bold"/>  
  38.             <Button Name="btnClose" Height="30" Width="30" Content="X" FontWeight="Bold" Foreground="Red" Cursor="Hand" Focusable="False"  
  39.                     HorizontalAlignment="Right" VerticalAlignment="Bottom" Background="{x:Null}" Margin="0,0,2,0" Click="BtnClose_Click"/>  
  40.         </Grid>  
  41.         <Grid Grid.Row="1">  
  42.             <TabControl Name="tabPlugs">  
  43.             </TabControl>  
  44.         </Grid>  
  45.         <Grid Grid.Row="2" Background="Gray">  
  46.   
  47.         </Grid>  
  48.     </Grid>  
  49. </Window>  
In MainWindow.xaml.cs, add the below code:
  1. using MEF.Sample.Plugger.Contract;  
  2. using System;  
  3. using System.Collections.Generic;  
  4. using System.ComponentModel.Composition;  
  5. using System.ComponentModel.Composition.Hosting;  
  6. using System.Configuration;  
  7. using System.IO;  
  8. using System.Linq;  
  9. using System.Windows;  
  10. using System.Windows.Controls;  
  11. using System.Windows.Input;  
  12.   
  13. namespace MEF.Sample.Calculator  
  14. {  
  15.     /// <summary>  
  16.     /// Interaction logic for MainWindow.xaml  
  17.     /// </summary>  
  18.     public partial class MainWindow : Window  
  19.     {  
  20.         public MainWindow()  
  21.         {  
  22.             InitializeComponent();  
  23.             LoadView();  
  24.             LoadUI();  
  25.         }  
  26.         private CompositionContainer _container;  
  27.   
  28.         [ImportMany(typeof(IPlugger))]  
  29.         private IEnumerable<Lazy<IPlugger>> _pluggers;  
  30.   
  31.         public void LoadView()  
  32.         {  
  33.             string plugName = ConfigurationSettings.AppSettings["Plugs"].ToString();              
  34.             var connectors = Directory.GetDirectories(plugName);  
  35.             var catalog = new AggregateCatalog();  
  36.             connectors.ToList().ForEach(connect => catalog.Catalogs.Add(new DirectoryCatalog(connect)));  
  37.             _container = new CompositionContainer(catalog);  
  38.             _container.ComposeParts(this);  
  39.         }  
  40.   
  41.         /// <summary>  
  42.         /// Load all IPlggers available in PlugBoard Folder  
  43.         /// </summary>  
  44.         public void LoadUI()  
  45.         {  
  46.             try  
  47.             {  
  48.                 TabItem buttonA = new TabItem();  
  49.                 buttonA.Header = "Welcome";  
  50.                 buttonA.Height = 30;  
  51.                 buttonA.Content = "You welcome :)";  
  52.                 tabPlugs.Items.Add(buttonA);  
  53.   
  54.                 foreach (Lazy<IPlugger> plug in _pluggers)  
  55.                 {  
  56.                     TabItem button = new TabItem  
  57.                     {  
  58.                         Header = plug?.Value?.PluggerName,  
  59.                         Height = 30,  
  60.                         Content = plug?.Value?.GetPlugger()  
  61.                     };  
  62.                     tabPlugs.Items.Add(button);  
  63.                 }  
  64.             }  
  65.             catch (Exception ex)  
  66.             {  
  67.                 MessageBox.Show(ex.Message, "Internal Error", MessageBoxButton.OK, MessageBoxImage.Error);  
  68.             }  
  69.   
  70.         }  
  71.   
  72.         private void BtnClose_Click(object sender, RoutedEventArgs e)  
  73.         {  
  74.             Close();  
  75.         }  
  76.   
  77.         private void Window_MouseDown(object sender, MouseButtonEventArgs e)  
  78.         {  
  79.             if (e.ChangedButton == MouseButton.Left)  
  80.                 this.DragMove();  
  81.         }  
  82.   
  83.         
  84.     }  
  85. }  
In App.Config add key to access the PlugBoard Folder. The PlugBoard is the folder were all calculator libraries will be placed.
  1. <?xml version="1.0" encoding="utf-8" ?>  
  2. <configuration>  
  3.     <startup>   
  4.         <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7" />  
  5.     </startup>  
  6.   <appSettings>  
  7.     <add key="Plugs" value="Path of PlugBoard Folder\PlugBoard\"/>  
  8.   </appSettings>  
  9. </configuration>  
To implement the MEF solution, we need the below two references:
  • System.ComponentModel.Composition
  • System.ComponentModel.Composition.Hosting 
In the above MainWindow.xaml.cs class, we can see that the method LoadView where we are reading files is available in the PlugBoard folder and adding in the container. The container will take all export providers. With this movement from the export keyword, MEF learns which class it has to load (lazily) in the main application. From Import/ImportMany, MEF will identify which variable it has to map the exported values.
 
This is the first snap of the Main window:
 
 
Now it is time to create plugs for the Calculator plugboard. Create a project of type class library. We will name it “MEF.RegularCalculator”.
 
Delete Class1.cs and create the View folder
 
Add a WPF UserControl to it with the name RegularView
 
Add Plugger Project as a reference to this project
 
Add the below assemblies as a reference;
  • System.ComponentModel.Composition
  • System.ComponentModel.Composition.Hosting 
In RegularView.xaml.cs, for the RegularView class, inherit the interface IPlugger and provide implementation as below:
  1. using MEF.Sample.Plugger.Contract;  
  2. using System.ComponentModel.Composition;  
  3. using System.Text.RegularExpressions;  
  4. using System.Windows;  
  5. using System.Windows.Controls;  
  6. using System.Windows.Input;  
  7.   
  8. namespace MEF.RegularCalculator.View  
  9. {  
  10.     /// <summary>  
  11.     /// Interaction logic for RegularView.xaml  
  12.     /// </summary>  
  13.     [Export(typeof(IPlugger))]  
  14.     public partial class RegularView : UserControl, IPlugger  
  15.     {  
  16.         public RegularView()  
  17.         {  
  18.             InitializeComponent();  
  19.         }  
  20.           
  21.         /// <summary>  
  22.         /// This is name will display in main plug board  
  23.         /// </summary>  
  24.         public string PluggerName { get; set; } = "Regular";  
  25.   
  26.         /// <summary>  
  27.         /// This will get called when user clicked on Regular option from plug board  
  28.         /// </summary>  
  29.         /// <returns></returns>  
  30.         public UserControl GetPlugger() => this;  
  31.   
  32.         private void BtnAdd_Click(object sender, RoutedEventArgs e)  
  33.         {  
  34.             lblRes.Content = int.Parse(txtA.Text) + int.Parse(txtB.Text);  
  35.         }  
  36.   
  37.         private void BtnSubstract_Click(object sender, RoutedEventArgs e)  
  38.         {  
  39.             lblRes.Content = int.Parse(txtA.Text) - int.Parse(txtB.Text);  
  40.         }  
  41.   
  42.         private void BtnMultiply_Click(object sender, RoutedEventArgs e)  
  43.         {  
  44.             lblRes.Content = int.Parse(txtA.Text) * int.Parse(txtB.Text);  
  45.         }  
  46.   
  47.         private void BtnDivide_Click(object sender, RoutedEventArgs e)  
  48.         {  
  49.             lblRes.Content = int.Parse(txtA.Text) / int.Parse(txtB.Text);  
  50.         }  
  51.   
  52.         private void AllowOnlyNumbers(object sender, TextCompositionEventArgs e)  
  53.         {  
  54.             Regex regex = new Regex("[^0-9]+");  
  55.             e.Handled = regex.IsMatch(e.Text);  
  56.         }  
  57.   
  58.     }  
  59. }  
We are implementing IPlugger here because when we load the Main Calculator application, the main application looks for the class which is being mentioned as Export. It gets the plugger name by the property PluggerName and loads the User controls on the main app using the method GetPlugger().
 
Now go to Project Properties -> Build and change the Output to PlugBoard folder. As we discussed earlier, PlugBoard is the folder where we are placing all our assemblies DLLs. In the main application, App.Config, we are configuring the path of the PlugBoard.
 
 
Build the entire solution and the “MEF.Sample.Calculator” project set as a startup project. Run the application, below is the output snap of window. Here you can see “Regular” is added:
 
 
 
Now we will follow by creating projects with the names “MEF.TableCalculator” and “MEF.TrigonometryCalculator”. Below is the output snap of window where you can now see 2 more options plugged to the main calculator application with the names “Table Calci” and “Trigonometry”
 
 
 
 
For your reference, the Project Solution is uploaded and you can download it. Before running the project, remember to configure the PlugBoard folder path in App.Config.
 
This is how we can just keep on adding the required plugs and build the project so it will be available for usage. After delivering to the customer, if the customer requires more options, we can keep on extending the calculator functionalities without touching the previous code. So basically, it will obey the Open/Closed Principle of solid principles (i.e. open for extension closed for modification.)
 

Summary

 
In this article, we have seen how we can develop a plug and play solution to meet our requirements which need extensibility using MEF Framework