Introduction
Here we have an example implementation of plug-in architecture project for the C# .NET 4.5 Winforms platform. This project was created in Visual Studio 2013, but will open and run in Visual Studio 2012 with no issues.
Background
I had heard about plug-in architecture a while ago and thought it was a good idea. So I decided to create an example C# Winforms plug-ins project, but with a realistic design for real world scenarios.
Bearing this in mind, I decided to create a plug-in system which had a user control, and a single drop-down menu, both of which were contained within one supervisory class. The project was designed in such a way that the host project was not aware of the plug-in classes' operations, and only loaded the plug-ins from a test class, or from an external class library (DLL).
The plug-ins were designed in such a way that the drop-down menu could send a typed event to the user control to tell it which drop-down item had been selected.
Using the Code
Here is an image of the solution with all of its projects:
The solution contains five projects, two Winforms host projects and three class libraries. The main and Winforms host project is the Winforms.Plugins.Host
which is where the plug-ins are loaded into, either from a local test class, or from a class library file (DLL).
There is also a test host project called Winforms.Plugins.DemoPlugin.TestHarness
, this is used for testing the plug-in class libraries before they are loaded into the host project.
The three class libraries consist of two demo plugin libraries, and a shared class library. I think their names denote which ones are which.
The host project (winforms.plugins.host
) only function is to load in the plugins and populate a Tab Control with the user controls from the plug-ins, and then to populate the Menu Strip control with each individual drop down menu or Menu Item.
The host project consists of a main form, an inherited User Control and a local Test Plugin class, as shown:
There is also a PluginsToConsume
folder, where the class libraries (DLLs) containing the plug-ins should be placed before start-up.
=================
Note
The demo plug-in class libraries copy their assemblies to the PluginsToConsume
folder in the host project as a Post-Build step shell script command. An example of which is shown below:
copy $(ProjectDir)\bin\Debug\*.* $(SolutionDir)Winforms.Plugins.Host\PluginsToConsume /y
As can be seen, relative paths have been used to ensure portability of the solution.
=================
The InheritedUserControl
will be explained later, as it's part of the plug-in class structure.
The Host Form's code behind file looks like this:
using System;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
using Microsoft.Practices.Unity;
using Winforms.Plugins.Shared;
namespace Winforms.Plugins.Host
{
public partial class HostForm : Form
{
IUnityContainer container = null;
private String pluginFilePath = String.Empty;
private Boolean testMode = false;
public HostForm()
{
InitializeComponent();
}
private void HostForm_Load(object sender, EventArgs e)
{
pluginFilePath = Directory.GetParent
(System.IO.Directory.GetCurrentDirectory()).Parent.FullName + @"\PluginsToConsume\";
testMode = Boolean.Parse(ConfigurationManager.AppSettings["TestMode"]);
hostTabControl.Visible = false;
if (testMode)
this.Text = "Test Mode";
else
this.Text = "Live Mode - Plugins Extracted From Assemblies";
}
private void btnLoadPlugins_Click(object sender, EventArgs e)
{
LoadPluginsFromContainer();
}
private void LoadPluginsFromContainer()
{
if (container != null)
{
hostTabControl.TabPages.Clear();
menuStripHost.Items.Clear();
var loadedPlugins = container.ResolveAll<IPlugin>();
if (loadedPlugins.Count() > 0)
hostTabControl.Visible = true;
foreach (var loadedPlugin in loadedPlugins)
{
menuStripHost.Items.Add(loadedPlugin.PluginControls().MenuStripItemContainer);
TabPage tabPage = new TabPage(loadedPlugin.Name());
tabPage.Controls.Add(loadedPlugin.PluginControls().UserControlContainer);
hostTabControl.TabPages.Add(tabPage);
}
}
}
private void btnEmptyContainer_Click(object sender, EventArgs e)
{
container = new UnityContainer();
hostTabControl.Visible = false;
hostTabControl.TabPages.Clear();
menuStripHost.Items.Clear();
}
private void btnLoadContainer_Click(object sender, EventArgs e)
{
container = new UnityContainer();
hostTabControl.Visible = false;
hostTabControl.TabPages.Clear();
menuStripHost.Items.Clear();
if (testMode)
{
container.RegisterInstance<IPlugin>
("Plugin 1", new TestPlugin("Test Plugin 1"));
container.RegisterInstance<IPlugin>
("Plugin 2", new TestPlugin("Test Plugin 2"));
}
else
{
string[] files = Directory.GetFiles(pluginFilePath, "*.dll");
Int32 pluginCount = 1;
foreach (String file in files)
{
Assembly assembly = Assembly.LoadFrom(file);
foreach (Type T in assembly.GetTypes())
{
foreach (Type iface in T.GetInterfaces())
{
if (iface == typeof(IPlugin))
{
IPlugin pluginInstance = (IPlugin)Activator.CreateInstance
(T, new [] {"Live Plugin " + pluginCount++});
container.RegisterInstance<IPlugin>
(pluginInstance.Name(), pluginInstance);
}
}
}
}
}
}
}
}
Once the host application establishes testMode
and pluginFilePath
member variables, it informs the user of the mode using its Form.Text
property. As can be seen, Test Mode is derived from the App.config
appSettings
section as a key.
Note: Again the pluginFilePath
member uses relative paths to ensure portability.
When the application is started, it looks like this:
As can be seen there are three buttons, the first is 'Instantiate Plugins Onto Container'. In Live Mode, this uses reflection to read the classes in the assemblies it finds. If they inherit from IPlugin
, an interface found in the Winforms.Plugins.Shared
class library, then it attempts to consume them as plug-ins.
Next the 'Load Plugins Into Form' button should be pressed, any plug-in data derived is then loaded into the host form. There is also an 'Empty Container' button which removes the stored plug-in data cached locally.
Note: A full explanation of the plug-in section of the solution will be included later.
In order to store the plug-in data, the host form uses a Microsoft Unity Dependency Injection container. I thought as the plug-in idea was similar to the IOC (Inversion Of Control) principle used in Dependency Injection, it seemed a good idea to use a DI container to store to plug-in data, and even though the DI container is only used as a data store between events, it is an efficient method.
The plugins are loaded onto the container in this way:
private void btnLoadContainer_Click(object sender, EventArgs e)
{
container = new UnityContainer();
hostTabControl.Visible = false;
hostTabControl.TabPages.Clear();
menuStripHost.Items.Clear();
if (testMode)
{
container.RegisterInstance<IPlugin>
("Plugin 1", new TestPlugin("Test Plugin 1"));
container.RegisterInstance<IPlugin>
("Plugin 2", new TestPlugin("Test Plugin 2"));
}
else
{
string[] files = Directory.GetFiles(pluginFilePath, "*.dll");
Int32 pluginCount = 1;
foreach (String file in files)
{
Assembly assembly = Assembly.LoadFrom(file);
foreach (Type T in assembly.GetTypes())
{
foreach (Type iface in T.GetInterfaces())
{
if (iface == typeof(IPlugin))
{
IPlugin pluginInstance = (IPlugin)Activator.CreateInstance
(T, new [] {"Live Plugin " + pluginCount++});
container.RegisterInstance<IPlugin>
(pluginInstance.Name(), pluginInstance);
}
}
}
}
}
}
The RegisterInstance<T>
method is used to register the plugin class instances against the container.
After this step, the 'Load Plugins Into Form' button should be pressed, the following code is then executed:
private void btnLoadPlugins_Click(object sender, EventArgs e)
{
LoadPluginsFromContainer();
}
private void LoadPluginsFromContainer()
{
if (container != null)
{
hostTabControl.TabPages.Clear();
menuStripHost.Items.Clear();
var loadedPlugins = container.ResolveAll<IPlugin>();
if (loadedPlugins.Count() > 0)
hostTabControl.Visible = true;
foreach (var loadedPlugin in loadedPlugins)
{
menuStripHost.Items.Add(loadedPlugin.PluginControls().MenuStripItemContainer);
TabPage tabPage = new TabPage(loadedPlugin.Name());
tabPage.Controls.Add(loadedPlugin.PluginControls().UserControlContainer);
hostTabControl.TabPages.Add(tabPage);
}
}
}
If the DI container is not empty, the host Tab control and Menu Strip controls have their items cleared, and then populated with the loaded plugins.
For each plugin class, the host application encounters it creates a new Tab Page, and drop down menu or Menu Item for each plugin. As can be seen below:
The above capture was done in Test Mode, the Live Mode plug-ins look like this when they are loaded:
In order to load the data into the first live plug-in, 'Live Plugin 1 -> Load Data' should be selected from the drop down menu.
As can be seen, there is mocked data loaded into a DataGridView
control. The data mocking was achieved with the use of NBuilder
data mocking extension available from the NuGet
package installer.
The data mocking is achieved with the following source code block:
public class MockData
{
public static DataTable GenerateDataTable<T>(int rows)
{
var datatable = new DataTable(typeof(T).Name);
typeof(T).GetProperties().ToList().ForEach(x => datatable.Columns.Add(x.Name));
Builder<T>.CreateListOfSize(rows).Build().ToList()
.ForEach(x => datatable.LoadDataRow(x.GetType().GetProperties()
.Select(y => y.GetValue(x, null)).ToArray(), true));
return datatable;
}
}
The returned DataTable
is bound to the DataGridView
control in the standard way. There is a second live plug-in which loads an image as a second simple example derived from a different assembly.
Plug-in Class
The plug-in class must inherit from an interface class called IPlugin
found in Winforms.Plugins.Shared
.
The definition of the interface is shown below:
public interface IPlugin
{
String Name();
ControlTemplate PluginControls();
}
An example plugin called Winforms.Plugins.DemoPlugin
is included and the definition is as follows:
public class DataGridViewPlugin : IPlugin
{
private ControlTemplate controlTemplate;
private String name = String.Empty;
public DataGridViewPlugin(String name)
{
this.name = name;
controlTemplate = new ControlTemplate(this.Name(),
new List<string>() { "Load Data" },
new DataGridViewUserControl());
}
public String Name()
{
return this.name;
}
public ControlTemplate PluginControls()
{
return controlTemplate;
}
}
Apart from the Name
property there is a ControlTemplate
the control template class is shown below:
public class ControlTemplate
{
public UserControlWithCallBack UserControlContainer;
public ToolStripMenuItem MenuStripItemContainer;
public ControlTemplate(String name, List<String>
dropDownMenuItemNames, UserControlWithCallBack pluginUserControl)
{
UserControlContainer = new UserControlWithCallBack();
UserControlContainer = pluginUserControl;
ToolStripMenuItem topLevelMenuStripItem = new ToolStripMenuItem(name);
foreach (String dropDownMenuItemName in dropDownMenuItemNames)
{
ToolStripMenuItem dropDownMenuStripItem = new ToolStripMenuItem(dropDownMenuItemName);
dropDownMenuStripItem.Click += new EventHandler(MenuItemClickHandler);
topLevelMenuStripItem.DropDownItems.Add(dropDownMenuStripItem);
}
MenuStripItemContainer = topLevelMenuStripItem;
}
private void MenuItemClickHandler(object sender, EventArgs e)
{
ToolStripMenuItem receivedMenuItem = (ToolStripMenuItem)sender;
UserControlContainer.ReceiveData(receivedMenuItem.Text);
}
}
The ControlTemplate
class has a constructor which accepts a name, a list of text items for the drop down menu, and a user control which inherits from the UserControlWithCallBack
class. This is a base class for the User Control used in the plug-in class, and is shown below:
public partial class UserControlWithCallBack : UserControl
{
public event EventHandler<EventArgs<String>> CallBack;
public UserControlWithCallBack()
{
InitializeComponent();
}
public void ReceiveData(String callBackData)
{
CallBack.SafeInvoke(this, new EventArgs<string>(callBackData));
}
}
The inherited User Control must then subscribe to the base class CallBack
event as follows:
public partial class DataGridViewUserControl : UserControlWithCallBack
{
public DataGridViewUserControl()
{
InitializeComponent();
base.CallBack += DataGridViewUserControl_CallBack;
}
void DataGridViewUserControl_CallBack(object sender, EventArgs<string> e)
{
if (e.Value == "Load Data")
{
DataTable testData = MockData.GenerateDataTable<Person>(50);
dataGridViewTest.DataSource = testData;
lblDescription.Visible = true;
dataGridViewTest.Visible = true;
}
}
}
This allows the User Control's Drop Down Menu to pass data to the inherited User Control.
Extension Methods
There are two public
methods used, they are shown below:
namespace System
{
public class EventArgs<T> : EventArgs
{
public EventArgs(T value)
{
_value = value;
}
private T _value;
public T Value
{
get { return _value; }
}
}
public static class Extensions
{
public static void SafeInvoke<T>
(this EventHandler<T> eventToRaise, object sender, T e) where T : EventArgs
{
EventHandler<T> handler = eventToRaise;
if (handler != null)
{
handler(sender, e);
}
}
}
}
The first method provides typed EventArgs
which is used between the drop down menu, and the user control in the plug-in class. The second is an extension method to allow for the thread safe use of events.
Finally
I hope this example proves to be useful to the programming community in some way, that was the intended purpose of the project.
The example project does not really have any error handling in it, but as this was designed to be a proof of concept, it was not felt necessary.
Points Of Interest
The only real point of interest I can think of is the use of inherited User Controls. This is something I had not used before, and when I realised the project structure I needed was pleasantly surprised to find inherited User Controls as a control template.
History