Click here to Skip to main content
15,860,859 members
Articles / Desktop Programming / Windows Forms

Plug-in Framework

Rate me:
Please Sign up or sign in to vote.
4.84/5 (25 votes)
17 May 2018CPOL4 min read 98.7K   1.9K   132   37
Basic framework for building desktop plug-in applications

UPDATE: 2018-05-17

This article was originally written 8 years ago. I now recommend using MEF (Managed Extensibility Framework) for these kinds of projects. However, some people may still find rolling their own more useful. As such, I have copied the code to GitHub for anyone who still finds it useful and may wish to fork the code.

Introduction

This article will demonstrate how to create a basic plug-in application for WinForms using the PluginFramework. This is not intended to be a perfect solution, but it is a mighty good start in that direction, if I must say so myself! It should help you get started on the right track. I have been meaning to write this for quite some time, but never had the time; in fact, I still don't have much time, so you'll have to forgive me if the article is somewhat lacking in elaboration!

PluginFramework/Capture.PNG

PluginFramework/Capture2.PNG

Interface

First of all, we need to define a common interface for loading plug-ins. All interfaces are defined in the PluginFramework.Interfaces project. This way, both the host and the plug-ins can reference a separate project (we wouldn't want the interfaces in the host application, as this would mean each plug-in built would need to reference the plug-in host!).

IPlugin

About as simple as it gets... this is the interface that will be used to make sure all the code can play nice.

C#
public interface IPlugin
{
    string Title { get; }
    string Description { get; }
    string Group { get; }
    string SubGroup { get; }
    XElement Configuration { get; set; }
    string Icon { get; }
    void Dispose();
}
  • Title: Name of the plug-in
  • Description: Obvious, right?
  • Group: This allows you to group related plug-ins together (think of a MenuStrip and menu items).
  • SubGroup: Not too hard to figure out what this is, is it?
  • Configuration: This allows you to pass configuration details to and from your plug-in. For example, the host could supply the configuration on plug-in load, and when disposing the project, the latest configuration can be passed back to the host for saving to disk for later use.
  • Icon: URI to an icon file, which can be used on a TreeView or MenuStrip control, for example.

IFormPlugin

C#
public enum ShowAs
{
    Normal,
    Dialog
}

public interface IFormPlugin: IPlugin
{
    Form Content { get; }
    ShowAs ShowAs { get; }
}

Now we get to specifics...

  • Content: A form control, which is to be loaded as a plug-in

IUserControlPlugin

C#
public interface IUserControlPlugin: IPlugin
{
    UserControl Content { get; }
}
  • Content: A user control, which is to be loaded as a plug-in

Attributes

This will be used when trying to load assembly files. Marking the assembly with the correct attributes means you are sure to not try to load a rogue DLL. It also helps you locate the control to load from the assembly.

C#
[AttributeUsage(AttributeTargets.Assembly)]
public class MainContentAttribute : Attribute
{
    public string Content { get; set; }
    public MainContentAttribute(string mainContent)
    {
        this.Content = mainContent;
    }
}

When creating plug-ins, simply add something like this to your AssemblyInfo file:

C#
[assembly: MainContent("DemoUserControlPlugin.UserControl1")]

where UserControl1 inherits IUserControlPlugin.

You could also add other attributes to check for plug-in versions, etc., but I will leave that up to you.

Utilities

Configuration File

The configuration file class allows you to easily load and save plug-in configuration, and even lets you specify which plug-ins to load on startup.

Below is an example configuration file:

XML
<ConfigurationFile>
 <Startup>
  <Plugin Title="DemoFormPlugin" 
     AssemblyPath="D:\My Documents\Visual Studio 2008\Projects\
                   PluginFramework\Demo\bin\Debug\
                   Plugins\DemoFormPlugin.dll" />
  <Plugin Title="UserControlTest" 
     AssemblyPath="D:\My Documents\Visual Studio 2008\Projects\
                   PluginFramework\Demo\bin\Debug\Plugins\
                   DemoUserControlPlugin.dll" />
 </Startup>
 <PluginConfiguration>
  <Plugin Title="DemoFormPlugin">
   <Configuration>
    <ThisFormConfig />
   </Configuration>
  </Plugin>
  <Plugin Title="UserControlTest">
   <Configuration>
    <UCConfig />
   </Configuration>
  </Plugin>
 </PluginConfiguration>
</ConfigurationFile>

The items <UCConfig /> and <ThisFormConfig /> relate to the configuration property in the IPlugin interface.

PluginHelper

Now this is where the real work gets done, and you'll be surprised at just how simple it is.

C#
public static class PluginHelper
{
    private static string pluginsDirectory = Path.GetDirectoryName(
        Assembly.GetExecutingAssembly().GetName().CodeBase).Substring(6);
    public static string PluginsDirectory
    {
        get { return pluginsDirectory; }
        set { pluginsDirectory = value; }
    }

    /// <summary>
    /// Returns a new plugin and the assembly location.
    /// </summary>
    /// <param name="file"></param>
    /// <returns></returns>
    public static PluginInfo AddPlugin(string file)
    {
        Assembly assembly = Assembly.LoadFile(file);
        //PluginVersionAttribute version =
        //   (PluginVersionAttribute)Attribute.GetCustomAttribute(assembly,
        // typeof(PluginVersionAttribute));
        //if (version != null && version.VersonNumber == "1.0.0")
        //{
            MainContentAttribute contentAttribute = 
              (MainContentAttribute)Attribute.GetCustomAttribute(
               assembly,typeof(MainContentAttribute));
            IPlugin plugin = 
              (IPlugin)assembly.CreateInstance(contentAttribute.Content, true);
            PluginInfo pluginInfo = new PluginInfo();
            pluginInfo.AssemblyPath = file;
            pluginInfo.Plugin = plugin;
            return pluginInfo;
        //}
        //else
        //{
            // return null;
            // //txtDescription.Text = "You tried to load an unsupported assembly";
        //}
    }

    /// <summary>
    /// Creates a new instance of the plugin inside the specified assembly file
    /// </summary>
    /// <typeparam name="T">Form / UserControl</typeparam>
    /// <param name="assemblyFile">The assembly file to load</param>
    /// <returns></returns>
    public static T CreateNewInstance<T>(string assemblyFile)
    {
        Assembly assembly = Assembly.LoadFile(assemblyFile);
        MainContentAttribute contentAttribute = 
          (MainContentAttribute)Attribute.GetCustomAttribute(assembly,
        typeof(MainContentAttribute));
        T item = (T)assembly.CreateInstance(contentAttribute.Content, true);
        return item;
    }

    /// <summary>
    /// <para>Looks for plugins in the directory
    /// specified by the PluginsDirectory</para>
    /// <para>property</para>
    /// </summary>
    /// <returns>an IDictionary with plugin Title
    /// as the Key and Assembly path as the Value</returns>
    public static IDictionary<string, string> FindPlugins()
    {
        Dictionary<string, string> plugins = 
                          new Dictionary<string, string>();
        PluginInfo pluginInfo;
        foreach (string file in Directory.GetFiles(PluginsDirectory))
        {
            FileInfo fileInfo = new FileInfo(file);
            if (fileInfo.Extension.Equals(".dll"))
            {
                try
                {
                    pluginInfo = AddPlugin(file);
                    plugins.Add(pluginInfo.Plugin.Title, file);
                }
                catch
                {
                }
            }
        }
        return plugins;
    }

    /// <summary>
    /// Gets all plug-ins from the PluginDirectory
    /// </summary>
    /// <returns></returns>
    public static IDictionary<string, PluginInfo> GetPlugins()
    {
        Dictionary<string, PluginInfo> plugins = 
                        new Dictionary<string, PluginInfo>();
        PluginInfo pluginInfo;
        foreach (string file in Directory.GetFiles(PluginsDirectory))
        {
            FileInfo fileInfo = new FileInfo(file);
            if (fileInfo.Extension.Equals(".dll"))
            {
                try
                {
                    pluginInfo = AddPlugin(file);
                    plugins.Add(pluginInfo.Plugin.Title, pluginInfo);
                }
                catch
                {
                }
            }
        }
        return plugins;
    }

    /// <summary>
    /// Gets the specified plugins
    /// </summary>
    /// <param name="pluginsToLoad">List of assembly paths</param>
    /// <returns></returns>
    public static IDictionary<string, PluginInfo> 
                    GetPlugins(IEnumerable<string> pluginsToLoad)
    {
        Dictionary<string, PluginInfo> plugins = 
                         new Dictionary<string, PluginInfo>();
        PluginInfo pluginInfo;
        foreach (string file in pluginsToLoad)
        {
            FileInfo fileInfo = new FileInfo(file);
            if (fileInfo.Extension.Equals(".dll"))
            {
                try
                {
                    pluginInfo = AddPlugin(file);
                    plugins.Add(pluginInfo.Plugin.Title, pluginInfo);
                }
                catch
                {
                }
            }
        }
        return plugins;
    }
}

public class PluginInfo
{
    public IPlugin Plugin { get; set; }
    public string AssemblyPath { get; set; }
}

Yes, it needs some work. If I had the time, I'd clean it up, but this is intended to give you a basic working framework for building a plug-in app. You are welcome to customize as you see fit. In any case, it works well enough.

Helper Controls

Just to be really helpful, here are some controls that will auto-load an IPlugin:

PluginMenuStrip

Simply add one of these to your form, call the AddPlugin method from your code-behind... and voila; you now have a new menu item that once clicked will activate your plug-in!

C#
public class PluginMenuStrip : MenuStrip
{
    public void AddPlugin(PluginInfo pluginInfo)
    {
        ToolStripMenuItem pluginItem = 
           new ToolStripMenuItem(pluginInfo.Plugin.Title);
        pluginItem.Tag = pluginInfo;
        if (!string.IsNullOrEmpty(pluginInfo.Plugin.Icon))
        {
            pluginItem.Image = Image.FromFile(pluginInfo.Plugin.Icon);
        }
        if (pluginInfo.Plugin is IFormPlugin)
        {
            pluginItem.Click += new EventHandler(pluginItem_Click);
        }
        if (!string.IsNullOrEmpty(pluginInfo.Plugin.SubGroup))
        {
            ToolStripMenuItem subGroup = 
               new ToolStripMenuItem(pluginInfo.Plugin.SubGroup);
            subGroup.DropDownItems.Add(pluginItem);
            if (!string.IsNullOrEmpty(pluginInfo.Plugin.Group))
            {
                ToolStripMenuItem group = 
                   new ToolStripMenuItem(pluginInfo.Plugin.Group);
                group.DropDownItems.Add(subGroup);
                this.Items.Add(group);
            }
            else
            {
                this.Items.Add(subGroup);
            }
        }
        else
        {
            this.Items.Add(pluginItem);
        }
    }
    void pluginItem_Click(object sender, EventArgs e)
    {
        ToolStripMenuItem menuItem = sender as ToolStripMenuItem;
        PluginInfo pluginInfo = menuItem.Tag as PluginInfo;
        IFormPlugin plugin = pluginInfo.Plugin as IFormPlugin;
        Form form = plugin.Content;
        if (form.IsDisposed)
        {
            form = 
              PluginHelper.CreateNewInstance<Form>(pluginInfo.AssemblyPath);
        }
        if (plugin.ShowAs == ShowAs.Dialog)
        {
            form.ShowDialog();
        }
        else
        {
            form.Show();
        }
    }
}

PluginTreeView

Pretty much the same code as with the PluginMenuStrip. This control will load a plug-in via the AddPlugin method. However, as there is no standard way to show a UserControl, you will have to write that code yourself from the plug-in host (your app). You can get the currently selected IPlugin from the current TreeNode's Tag property.

C#
public class PluginTreeView: TreeView
{
    ImageList imageList = new ImageList();
    protected override void OnCreateControl()
    {
        base.OnCreateControl();
        this.ImageList = imageList;
        imageList.Images.Add(Resources.Tree);
    }
    public void AddPlugin(PluginInfo pluginInfo)
    {
        TreeNode pluginItem = new TreeNode(pluginInfo.Plugin.Title);
        pluginItem.Tag = pluginInfo;
        if (!string.IsNullOrEmpty(pluginInfo.Plugin.Icon))
        {
            imageList.Images.Add(new Icon(pluginInfo.Plugin.Icon));
            pluginItem.ImageIndex = imageList.Images.Count - 1;
            pluginItem.SelectedImageIndex = imageList.Images.Count - 1;
        }
        if (!string.IsNullOrEmpty(pluginInfo.Plugin.SubGroup))
        {
            TreeNode subGroup = new TreeNode(pluginInfo.Plugin.SubGroup);
            subGroup.Nodes.Add(pluginItem);
            if (!string.IsNullOrEmpty(pluginInfo.Plugin.Group))
            {
                TreeNode group = new TreeNode(pluginInfo.Plugin.Group);
                group.Nodes.Add(subGroup);
                this.Nodes.Add(group);
            }
            else
            {
                this.Nodes.Add(subGroup);
            }
        }
        else
        {
            this.Nodes.Add(pluginItem);
        }
    }
}

An Example

An example IUserControlPlugin:

C#
public partial class UserControl1 : UserControl, IUserControlPlugin
{
    public UserControl1()
    {
        InitializeComponent();
    }
    private void button1_Click(object sender, EventArgs e)
    {
        MessageBox.Show("You clicked button 1!");
    }
    private void button2_Click(object sender, EventArgs e)
    {
        MessageBox.Show("You clicked button 2!");
    }
    #region IUserControlPlugin Members
    public UserControl Content
    {
        get { return this; }
    }
    #endregion

    #region IPlugin Members

    public string Title
    {
        get { return "UserControlTest"; }
    }
    public string Description
    {
        get { return "Info about this user control plugin"; }
    }
    public string Group
    {
        get { return "UCGroup"; }
    }
    public string SubGroup
    {
        get { return "UCSubGroup"; }
    }
    private XElement configuration = new XElement("UCConfig");
    public XElement Configuration
    {
        get { return configuration; }
        set { configuration = value; }
    }
    public string Icon
    {
        get { return "C:\\Icons\\Globe.ico"; }
    }

    #endregion

}

And a demo plug-in host:

C#
public partial class DemoForm : Form
{
    private ConfigurationFile configFile = null;
    private IDictionary<string, PluginInfo> plugins = null;
    private IDictionary<string, string> startupPlugins = null;

    public DemoForm()
    {
        InitializeComponent();
        PluginHelper.PluginsDirectory = 
           Path.Combine(Application.StartupPath, "Plugins");
    }

    private void pluginTreeView_AfterSelect(object sender, TreeViewEventArgs e)
    {
        if (e.Node.Tag == null)
        { return; }
        PluginInfo pluginInfo = e.Node.Tag as PluginInfo;
        if (pluginInfo.Plugin is IUserControlPlugin)
        {
            UserControl control = ((IUserControlPlugin)pluginInfo.Plugin).Content;
            splitContainer1.Panel2.Controls.Clear();
            splitContainer1.Panel2.Controls.Add(control);
            control.Dock = DockStyle.Fill;
        }
        else if (pluginInfo.Plugin is IFormPlugin)
        {
            IFormPlugin formPlugin = (IFormPlugin)pluginInfo.Plugin;
            Form form = formPlugin.Content;
            if (form.IsDisposed)
            {
                form = PluginHelper.CreateNewInstance<Form>(
                                        pluginInfo.AssemblyPath);
            }
            if (formPlugin.ShowAs == ShowAs.Dialog)
            {
                form.ShowDialog();
            }
            else
            {
                form.Show();
            }
        }
    }

    private void LoadPlugins(IEnumerable<string> assemblyPaths)
    {
        plugins = PluginHelper.GetPlugins(assemblyPaths);
        foreach (PluginInfo pluginInfo in plugins.Values)
        {
            if (pluginInfo.Plugin is IFormPlugin)
            {
                pluginMenuStrip.AddPlugin(pluginInfo);
                pluginTreeView.AddPlugin(pluginInfo);
            }
            else if (pluginInfo.Plugin is IUserControlPlugin)
            {
                pluginTreeView.AddPlugin(pluginInfo);
            }
        }
    }
    private void mnuToolsOptions_Click(object sender, EventArgs e)
    {
        AvailablePluginsForm form = new AvailablePluginsForm();
        if (form.ShowDialog() == DialogResult.OK)
        {
            startupPlugins = form.SelectedPlugins;
            LoadPlugins(form.SelectedPlugins.Values);
        }
    }

    private void DemoForm_Load(object sender, EventArgs e)
    {
        if (File.Exists(Settings.Default.PluginConfigFile))
        {
            configFile = ConfigurationFile.Load(Settings.Default.PluginConfigFile);
            LoadPlugins((from x in configFile.Startup.Plugins
                select x.AssemblyPath).ToList());
        }
        else
        {
            configFile = new ConfigurationFile();
            configFile.Save(Settings.Default.PluginConfigFile);
        }
    }
    protected override void OnClosing(CancelEventArgs e)
    {
        if (startupPlugins != null)
        {
            foreach (KeyValuePair<string, string> kv in startupPlugins)
            {
                if (!configFile.Startup.Plugins.Contains(kv.Key))
                {
                    StartupPlugin plugin = new StartupPlugin();
                    plugin.Title = kv.Key;
                    plugin.AssemblyPath = kv.Value;
                    configFile.Startup.Plugins.Add(plugin);
                }
            }
        }
        foreach (KeyValuePair<string, PluginInfo> kv in plugins)
        {
            PluginConfig config = configFile.PluginConfiguration.Plugins[kv.Key];
            if (config == null)
            {
                config = new PluginConfig();
                config.Title = kv.Key;
                configFile.PluginConfiguration.Plugins.Add(config);
            }
            config.Configuration = kv.Value.Plugin.Configuration;
        }
        configFile.Save(Settings.Default.PluginConfigFile);
        base.OnClosing(e);
    }
}

And there you have it! Yes, I may get less points for not writing more words, but you can't complain that it isn't an easy-to-use framework! ;-) Enjoy! And if you have any improvements, I would be more than happy to hear them.

History

v1.1 - 2010 09 14

Due to various requests, I have updated the code with a newer version that addresses some bugs; yes, those icons are fixed (I never did quite get why something so off scope of the article was so important to some, but hey...). There was also an issue when loading the host the first time around; it would sometimes crash when closing. This is now resolved.

As an added bonus, there is now an ISettingsPlugin interface so that the user can change settings as well as a SettingsForm that will load the settings plugins for you (You could always make your plugin tabbed, but I think that just gets messy). To create a Settings plugin, do the same as with a regular plugin:

  1. Create your user control.
  2. Implement ISettingsPlugin.
  3. Add Content Attribute to Assembly using the SettingsContentAttribute instead of the MainContentAttribute.
  4. Away you go.
  5. When you're done, test it with the Host.exe by clicking on the Plugin Settings menu item.

License

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


Written By
Software Developer (Senior) Freelancer
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMessage Closed Pin
7-Jul-10 12:36
_beauw_7-Jul-10 12:36 
GeneralRe: My vote of 3 Pin
vnmatt7-Jul-10 13:13
vnmatt7-Jul-10 13:13 
GeneralThanks Pin
gutierrezgileta5-Jul-10 9:56
gutierrezgileta5-Jul-10 9:56 
GeneralMy vote of 5 Pin
gutierrezgileta5-Jul-10 9:55
gutierrezgileta5-Jul-10 9:55 
GeneralMy vote of 3 Pin
JasonShort5-Jul-10 6:01
JasonShort5-Jul-10 6:01 
GeneralNot enough content Pin
#realJSOP4-Jul-10 2:07
mve#realJSOP4-Jul-10 2:07 
GeneralRe: Not enough content Pin
vnmatt4-Jul-10 12:49
vnmatt4-Jul-10 12:49 
GeneralRe: Not enough content Pin
Dave Kreskowiak4-Jul-10 16:51
mveDave Kreskowiak4-Jul-10 16:51 
Code comments don't convey concepts. Relying on them for explanation does not make for a good article.

A guide to posting questions on CodeProject[^]



Dave Kreskowiak
Microsoft MVP
Visual Developer - Visual Basic
     2006, 2007, 2008
But no longer in 2009...




GeneralRe: Not enough content Pin
vnmatt5-Jul-10 1:07
vnmatt5-Jul-10 1:07 
GeneralRe: Not enough content Pin
Marc Clifton5-Jul-10 10:32
mvaMarc Clifton5-Jul-10 10:32 
GeneralRe: Not enough content Pin
vnmatt5-Jul-10 13:04
vnmatt5-Jul-10 13:04 
GeneralRe: Not enough content Pin
vnmatt5-Jul-10 13:06
vnmatt5-Jul-10 13:06 
GeneralRe: Not found 2 *.ico files [modified] Pin
RAND 4558665-Jul-10 21:58
RAND 4558665-Jul-10 21:58 

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.