Click here to Skip to main content
15,885,244 members
Articles / Programming Languages / C#

C# Winforms Plug-ins Architecture Example

Rate me:
Please Sign up or sign in to vote.
4.95/5 (19 votes)
31 Jan 2014CPOL6 min read 56.6K   2.9K   52   21
C# .NET 4.5 Winforms Plugin Architecture Example

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:

Image 1

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:

Image 2

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:

C#
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);
                            }
                        }
                    }
                }
            }
            // At this point the unity container has all the plugin data loaded onto it. 
        }
    }
}

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:

Image 3

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:

C#
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);
                    }
                }
            }
        }
    }
    // At this point the unity container has all the plugin data loaded onto it.
}

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:

C#
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:

form

The above capture was done in Test Mode, the Live Mode plug-ins look like this when they are loaded:

Form

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:

C#
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:

C#
public interface IPlugin
{
    String Name();
    ControlTemplate PluginControls();
}

An example plugin called Winforms.Plugins.DemoPlugin is included and the definition is as follows:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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

  • Version 1.0

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United Kingdom United Kingdom
I am a software developer working with Microsoft technologies since 2000.

Skills include C/C++, C#, VB.NET, MS SQL Server, ASP.NET Webforms/MVC, AngularJS, NodeJS, Sharepoint Development, WPF, WCF, Winforms, plus others.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Member 1544345425-Nov-21 6:17
Member 1544345425-Nov-21 6:17 
GeneralRe: My vote of 5 Pin
Baxter P25-Nov-21 7:44
professionalBaxter P25-Nov-21 7:44 
QuestionDialog to HostForm Pin
Z@clarco10-Feb-16 2:26
Z@clarco10-Feb-16 2:26 
GeneralMy vote of 5 Pin
Ben23825-Nov-15 4:18
Ben23825-Nov-15 4:18 
QuestionC# 6 Update ;-) Pin
Ehouarn29-Sep-15 16:18
Ehouarn29-Sep-15 16:18 
AnswerRe: C# 6 Update ;-) Pin
Baxter P19-Oct-15 11:00
professionalBaxter P19-Oct-15 11:00 
Yes, you're right. That feature was needed, well done.
QuestionMy vote of #5 Pin
BillWoodruff30-Jan-15 3:56
professionalBillWoodruff30-Jan-15 3:56 
Questionwhen I run the project, throw out a exception. Pin
xing201323-Jan-15 0:36
xing201323-Jan-15 0:36 
AnswerRe: when I run the project, throw out a exception. Pin
Baxter P23-Jan-15 12:10
professionalBaxter P23-Jan-15 12:10 
GeneralRe: when I run the project, throw out a exception. Pin
xing201323-Jan-15 19:48
xing201323-Jan-15 19:48 
QuestionGreat Pin
Pefik24-Jun-14 21:45
Pefik24-Jun-14 21:45 
NewsVery good! Pin
luyongqiang201124-Feb-14 20:39
luyongqiang201124-Feb-14 20:39 
Questionplug-ins vs. CAB Pin
Andrey Dryazgov2-Feb-14 23:19
professionalAndrey Dryazgov2-Feb-14 23:19 
AnswerRe: plug-ins vs. CAB Pin
Baxter P3-Feb-14 1:05
professionalBaxter P3-Feb-14 1:05 
Questioninteresting ... how about unloading a plug-in ? Pin
BillWoodruff31-Jan-14 12:58
professionalBillWoodruff31-Jan-14 12:58 
AnswerRe: interesting ... how about unloading a plug-in ? Pin
Baxter P31-Jan-14 14:00
professionalBaxter P31-Jan-14 14:00 
GeneralRe: interesting ... how about unloading a plug-in ? Pin
SpArtA1-Feb-14 22:33
SpArtA1-Feb-14 22:33 
GeneralRe: interesting ... how about unloading a plug-in ? Pin
Baxter P2-Feb-14 0:20
professionalBaxter P2-Feb-14 0:20 
GeneralRe: interesting ... how about unloading a plug-in ? Pin
SpArtA2-Feb-14 3:01
SpArtA2-Feb-14 3:01 
GeneralRe: interesting ... how about unloading a plug-in ? Pin
Baxter P2-Feb-14 3:30
professionalBaxter P2-Feb-14 3:30 
GeneralRe: interesting ... how about unloading a plug-in ? Pin
SpArtA2-Feb-14 21:51
SpArtA2-Feb-14 21:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.