articles
A Plugin Architecture
Mar 08th 2012 Mogens Plugin C#

Preface

I have mentioned on my blog, I plan to bring back my article section where I write about stuff that interest me. In a previous incarnation of this site I had an article on Plugin Architecture, which I am republishing on here now. Granted it is from way back in 2008 and therefore a little old, but the concept still holds true. I will try to get an updated version which also contains multiple plugins to build on the concept but for now you can enjoy some .Net 2.0 code.

Read more →

Introduction

If you have ever completed a professional project I'm sure you have encountered the need for additional features 6 months down the road. Normally that would mean that you would have to refresh your memory on what you did and then try to fit the changes into the existing program. The need for applications to continuously adapt to changes in requirements have made the plugin architecture much more popular. As a matter of fact most of the applications you use in your daily endeavors use plugins in one way or another.

Isn't it too difficult?

As you will see the time spent up front is minimal in comparison to the benefits achieved later. The plugins will be loaded at runtime so you will be able to easily extend your applications. The architecture is based on interfaces so even if you have to hand it over to someone else they should able to pick up where you left off with no problem.

Before we start

This project was made in C# using Microsoft Visual Studio 2005 and the .NET 2.0 runtime. If for some reason you are using a different environment you will have to figure out what/how to replace on your own. Even if you don't have VS 2005 you should be able to walk away with the general idea of how plugins work. I'm not going to spend much time on the settings of all the components I use since I have included all the project files at the end of this article.

Interfaces

If you haven't yet worked with interfaces don't worry its easy. An interface is merely an abstract class with purely public methods, properties and events. You can consider it the architectural drawing of the application. Using interfaces makes it easy to enforce specific implementations since any class which inherits from an interface has to implement all methods of all the interfaces it implements( yes multiple interfaces can be inherited in one class).

Our Interface

The plugin we are about to make will assume that the main application has 2 panels (1 left and 1 right) which each can contain a UserControl. To create the interface just create a new project and select class library. Get rid of the autogenerated class and add your interface instead.

using System;
using System.Collections;

/// 
/// Plugin interface
/// 
namespace PluginInterface
{    
    public interface IPluginBase
    {
        string Name { get; set;}
        string Version { get; set;}
        string Author { get; set;}
        string Description { get; set;}
    };

    public enum MenuType
    { 
        mtMenuItem,
        mtToolbar,
        mtBoth,
        mtUnknown
    }

    public interface IPluginMenuItem : IPluginBase
    {
        string Text { get; set;}
        string ToolTip { get; set;}
        MenuType Type { get; set;}
        IPluginMenuItem[] PluginMenu { get; set;}
        System.EventHandler onclick { get; set;}
    };

    public interface IPlugin : IPluginBase
    {
        IPluginMenuItem[] PluginMenu { get;}
        System.Windows.Forms.UserControl LeftPanel { get;}
        System.Windows.Forms.UserControl RightPanel { get;}
    }
}
		
	                    

As you can see we have created a IPluginBase interface first which contains properties we wish all our plugins to contain. Next we have created a IPluginMenuItem which will allow us to add menu items and toolbar items to the host application. Please note the line "IPluginMenuItem[] PluginMenu { get; set;}". This will make it possible for anyone using our plugin to create the desired structure in the menu. Finally the actual interface contains the collection of menuitems and the property for the left and right panels.

The Plugin

The plugin we will make here is a IE clone. We will make a web browser with a listbox on the left panel for the bookmarks and the browser window in the right panel. Since this will be an assembly we will create a project with a class library. I named mine WebbrowserPlugin and the class webbrowserplgin but you can call yours whatever you want. We need to add the reference to the interfaces which can be done by right clicking on your project in the solution explorer and then selecting "Add Reference". You can then browse to the interface project you created earlier. Next I have set the class to inherit from IPlugin. (Tip : in VS 2005 you can right click on the interface and select implement and the IDE will create the methods and properties for you). After filling out the methods for set and get I added 2 forms to the project (Rightclick the project in the solution explore then select "Add" and "Windows Form"). The first form I named BookmarkFm and to that one I added a listbox named bookmarkslistBox and a toolstrip named addtoolStri. I also added a button to the toolstrip which I named "Add".

BookmarkFm

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace WebbrowserPlugin
{
    public partial class BookmarkFm : System.Windows.Forms.UserControl
    {
        private DataSet bookmarksds;
        private string Path;
        public WebBrowserPlgIn host;
        public BookmarkFm()
        {
            InitializeComponent();
            Path = System.IO.Path.GetDirectoryName(
			System.Reflection.Assembly.GetExecutingAssembly().Location);
            bookmarksds = new DataSet();
            LoadBookmarks();
        }

        private void LoadBookmarks()
        {
            bookmarksds.ReadXml(Path + @"\bookmarks.xml");
            bookmarkslistBox.DataSource = bookmarksds.Tables["Bookmark"];
            bookmarkslistBox.DisplayMember = "URL";
        
        }
        private void bookmarkslistBox_DoubleClick(object sender, EventArgs e)
        {
            if (host.RightPanel is BrowserFm)
            {
                string url = bookmarkslistBox.Text;
                (host.RightPanel as BrowserFm).LoadURL(url);
            }            
        }

        private Boolean ValueExists(DataSet ds, string FieldName, string Value)
        {
            int FieldIdx = -1;
            Boolean Res = false;            
                        
            if (ds.Tables[0].Columns.Contains(FieldName))
            {
                FieldIdx = ds.Tables[0].Columns.IndexOf(FieldName);
                foreach (DataRow dr in ds.Tables[0].Rows)
                {
                    if (dr[FieldIdx].Equals(Value))
                    {
                        Res = true;
                        break;
                    }
                }
            }
            return Res;
        }

        private void AddBtn_Click(object sender, EventArgs e)
        {
            if (!ValueExists(bookmarksds,"URL",(host.RightPanel as BrowserFm).CurrentURL))
            {
                DataTable dt = bookmarksds.Tables["Bookmark"];            
                dt.Rows.Add(new object[] { (host.RightPanel as BrowserFm).CurrentURL });
                bookmarksds.WriteXml(Path + @"\bookmarks.xml");
            }
        }
    }
}  
                                

As you can see the bookmarkslistBox contains the bookmarks we have. Those bookmarks are stored in a XML which is then bound to the control. The "Add" button will add the value in the url (from the browser form) to the xml if the url hasn't already been added (the ValueExists method). Also note that we added a "host" variable so that we can get access to the browser form. This is needed to allow us to load a page when we double click an item in the bookmarkslistBox. So not really much going on here :) The second form I named BrowserFm and on that one I added a Webbrowser control named webBrowser and a toolstrip named wbtoolStrip. To the toolstrip was added a button and a textbox.

BrowserFm

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace WebbrowserPlugin    
{
    public partial class BrowserFm : System.Windows.Forms.UserControl
    {
        public BrowserFm()
        {
            InitializeComponent();
           
        }

        public string Path;
        public string CurrentURL
        {
            get { return urlTextBox.Text; }
        }

        public void LoadURL(string url)
        {
            if (url.Trim() != "")
            {
                webBrowser.Navigate(url);
                urlTextBox.Text = url;                
            }
        }
        public void RefreshSite()
        {
            webBrowser.Refresh();
        }

        public void StopSite()
        {
            webBrowser.Stop();
        }

        private void gobtn_Click(object sender, EventArgs e)
        {
            LoadURL(urlTextBox.Text);
        }

        private void urlTextBox_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == (char)13)
            {
                LoadURL(urlTextBox.Text); 
            }
        }
    }
}	  
	                        

This form has even less code. Besides the 3 public methods for loading,stopping and refreshing a page we have implemented the click event of the button (to load the url) and the keypress of the textbox (to load the url on hitting enter) Lets finally get to the main code of this plugin.

WebBrowserPlgIn

Before looking at the constructor we need to do one more thing. If you guessed I was talking about the Menuitems you were right. We need to create a class for the MenuItem which inherits from the IPluginMenuItem interface. Again just rightclick on the interface and select "Implement". There is no need for a constructor here since we will fill it all from the WebBrowserPlgIn constructor so just fill out the autogenerated methods. All we are left with now is making the constructor of the WebBrowserPlgIn.

        private void OnReload (object sender, EventArgs e)
        {
            (rightpnl as BrowserFm).RefreshSite();
        }
        private void OnStop (object sender, EventArgs e)
        {
            (rightpnl as BrowserFm).StopSite();
        }
        public WebBrowserPlgIn()
        { 
            this.Author = @"Mogens Nielsen"; 
            this.Name = @"Webbrowser"; 
            this.Version = @"1.0.0";
            this.Description = "Plugin for browsing the web";
            leftpnl = new LeftFm();
            (leftpnl as LeftFm).host = this;
            rightpnl = new BrowserFm();
            (rightpnl as BrowserFm).LoadURL("www.msn.com");
            menuitems = new IPluginMenuItem[1];
            menuitems[0] = new Menuitem();
            menuitems[0].Name = "View";            
            menuitems[0].Text = "View";
            menuitems[0].Type = MenuType.mtMenuItem;

            menuitems[0].PluginMenu = new IPluginMenuItem[2];
            menuitems[0].PluginMenu[0] = new Menuitem();
            menuitems[0].PluginMenu[0].Name = "Stop";
            menuitems[0].PluginMenu[0].Text = "Stop";
            menuitems[0].PluginMenu[0].ToolTip = "Stop loading site";
            menuitems[0].PluginMenu[0].Type = MenuType.mtBoth;
            menuitems[0].PluginMenu[0].onclick += OnStop;

            menuitems[0].PluginMenu[1] = new Menuitem();
            menuitems[0].PluginMenu[1].Name = "Refresh";
            menuitems[0].PluginMenu[1].Text = "Refresh";
            menuitems[0].PluginMenu[1].ToolTip = "Refresh page";
            menuitems[0].PluginMenu[1].Type = MenuType.mtBoth;
            menuitems[0].PluginMenu[1].onclick += OnReload;
        }
	                        

Please note that leftpnl,rightpnl and menuitems are private variables for the globals LeftPanel,RightPanel and PluginMenu respectably. The OnReload and OnStop events are assigned to the eventhandler of each menuitem so that when a user click on the menu on the host it will execute the event we need. I know this seems like a lot but it really isn't. Most of the code in this assembly is from the interface. Anyway its time for the host.

Host

After creating a normal windows application project I have added the windows reference as well as the reference to our interfaces. We can now layout the form and for that I added a splitcontainer named MainSplitCon which I set to horizontal split and locked the panel1 control. Next I added another splitcontrol named PluginSplitCon to the panel2 of MainSplitCon. On PluginSplitCon I again locked panel1 (the left). I also added a menustrip which I named MainMenu and a toolstrip named MainToolStrip which was added to the panel1 of MainSplitCon. Now that we have a nice form to work with lets add some code. First we need to create a couple of variables we need later on

	  IPlugin CurrPlugin;
	  IPlugin[] Plugins;
	  string DataPath = Path.GetDirectoryName(Application.ExecutablePath) + @"\Plugins\";
	                        

CurrPlugin will hold the current selected plugin. Plugins is a collection of all the plugins we have. Finally we need a datapath for were to load the plugins from, which in this case is a folder named "plugins" right under the execution folder of the host application.

private void GetPlugins(string Path)
{
    if (!Directory.Exists(Path))
    {
        return;
    }

    Plugins = new IPlugin[Directory.GetFiles(Path, "*.dll").Length];            
    int counter = 0;

    foreach (string f in Directory.GetFiles(Path, "*.dll"))
    {
        Assembly asm = null;                

        asm = Assembly.LoadFrom(f);
        if (asm != null)
        {
            foreach (Type pluginType in asm.GetTypes())
            {
                if ((pluginType.IsPublic) || (!pluginType.IsAbstract))
                {
                    Type PluginClass = pluginType.GetInterface("PluginInterface.IPlugin", true);
                    
                    if (PluginClass != null)
                    {
                        Plugins[counter] = (IPlugin)Activator.CreateInstance(
						asm.GetType(pluginType.ToString()));
                        counter++;
                        AddPluginToMenu(Plugins[counter - 1].Name);
                    }
                }
            }                    
        }                                             
    }
}	                        

Here we run through all the *.dll files in the selected folder. If the file has has a public non abstract type that implements our interface "PluginInterface.IPlugin" we load it into our collection. With this check we make sure that we don't load something we can't use. Please note that in this project I load all the plugins at one time. If you have several plugins it may be better to load them on demand. If you look at the code you probably wonder what the "AddPluginToMenu" method is for so lets look at that

  private void AddPluginToMenu(string itemname)
  {
      ToolStripItem newitem;
      newitem = fileToolStripMenuItem.DropDownItems.Add(itemname, null, PluginMenuItem_Click);
      fileToolStripMenuItem.DropDownItems.Insert(0, newitem);
  }	  
                            

When I created the form I added 2 items to the MainMenu "fileToolStripMenuItem" and "aboutToolStripMenuItem". To the fileToolStripMenuItem I added a subitem "Exit" in which the click method was implemented to exit the application. Back to the code above you can see that I create a new subitem for the fileToolStripMenuItem with the name of plugin (itemname) and set its click event to "PluginMenuItem_Click". So lets see what happens in "PluginMenuItem_Click"

   private void PluginMenuItem_Click(object sender, EventArgs e)
   {
       ToolStrip parent = (sender as ToolStripMenuItem).Owner;
       ToolStripMenuItem item = (sender as ToolStripMenuItem);

       int index = parent.Items.IndexOf(item);

       if (index < Plugins.Length)
       {
           InitializePlugin(index);
       }            
   }                        

Here we locate the index of the item that was clicked and if its less than the total numbers of plugins loaded we call the method "InitializePlugin" with the index. So lets look at that method.

	  
	    private void InitializePlugin(int idx)
        {
            // clear current display
            PluginsplitCon.Panel1.Controls.Clear();
            PluginsplitCon.Panel2.Controls.Clear();
            MaintoolStrip.Items.Clear();            
            MainMenu.Items.Clear();
            // reattach file  menuitem
            MainMenu.Items.Add(fileToolStripMenuItem);
            

            CurrPlugin = Plugins[idx];
            CurrPlugin.LeftPanel.Dock = DockStyle.Fill;
            CurrPlugin.RightPanel.Dock = DockStyle.Fill;
            PluginsplitCon.Panel1.Controls.Add(CurrPlugin.LeftPanel);
            PluginsplitCon.Panel2.Controls.Add(CurrPlugin.RightPanel);
            foreach (IPluginMenuItem pm in CurrPlugin.PluginMenu)
            {
                ToolStripMenuItem tsi = new ToolStripMenuItem();
                tsi.Name = pm.Name;
                tsi.Text = pm.Text;
                tsi.ToolTipText = pm.ToolTip;
                switch (pm.Type)
                {                
                    case MenuType.mtToolbar  :   MaintoolStrip.Items.Add(tsi);
                                                 break;    
                    case MenuType.mtMenuItem :   MainMenu.Items.Add(tsi);
                                                 break;  
                    case MenuType.mtBoth     :   MaintoolStrip.Items.Add(tsi);
                                                 MainMenu.Items.Add(tsi);   
                                                 break;      
                }
                AddSubitems(pm, tsi);
            }

            // reattach file  menuitem
            MainMenu.Items.Add(aboutToolStripMenuItem);
        }
                            

First we need to remove the old plugin from our form as well as clearing the the menu and toolstrip (first 4 lines). However we still need the "File" menuitem so we re-attach that one. Next we set the current plugin "CurrPlugin" to the plugin in our collection matching the index that was passed. After making sure the panels fill the client (DockStyle.Fill) we can assign the 2 panels. Now comes the time to run through all the plugins menuitems and attach them to the hosts menu and/or toolstrip respectably. Unfortunately Microsoft decided to make the subitems of a menu item different than the top item so we have to create a separate method to handle that (AddSubitems).

private void AddSubitems(IPluginMenuItem pMenuItem, 
ToolStripMenuItem parentitem)
{
    ToolStripItem newitem = null;            

    foreach (IPluginMenuItem pm in pMenuItem.PluginMenu)
    {               
        switch (pm.Type)
        {
            case MenuType.mtToolbar: 
              newitem = new ToolStripButton();
              newitem.Name = pm.Name;
              newitem.Text = pm.Text;
              newitem.ToolTipText = pm.ToolTip;                                                
              MaintoolStrip.Items.Add(newitem);
              break;
            case MenuType.mtMenuItem:   
              newitem = new ToolStripMenuItem();                
              newitem.Name = pm.Name;
              newitem.Text = pm.Text;
              newitem.ToolTipText = pm.ToolTip;
              parentitem.DropDownItems.Add(newitem);
              break;
            case MenuType.mtBoth:       
              newitem = new ToolStripButton();
              newitem.Name = pm.Name;
              newitem.Text = pm.Text;
              newitem.ToolTipText = pm.ToolTip;
              newitem.Click += pm.onclick;
              MaintoolStrip.Items.Add(newitem);
              newitem = new ToolStripMenuItem();
              newitem.Name = pm.Name;
              newitem.Text = pm.Text;
              newitem.ToolTipText = pm.ToolTip;
              newitem.Click += pm.onclick;
              parentitem.DropDownItems.Add(newitem);
              break;
        }
    }
}                           

This method is very similar to the code in InitializePlugin() except here we add the items to the "DropDownItems" rather than "Items" Well we made it to the finish line so lets complete the constructor.

public MainFm() { InitializeComponent(); GetPlugins(DataPath); InitializePlugin(0); this.WindowState = FormWindowState.Maximized; }

Nothing to this : We load the plugins from the DataPath then call InitializePlugin(0) to show the first plugin. Finally we maximize the application. That's it we are DONE! Build the application and run it. Don't forget to move the plugin (and XML file) to the correct folder.

Conclusion

Granted this was a small application and it is not ready to be a commercial product. It also lacked error handling (which was left out on purpose for making this demo). However I still believe it proved how little work is needed up front to get great benefits later. Anytime you want to extend your application you only need to worry about implementing the interface and the host application will handle the rest. You can even publish your interface to allow your users or other developers to extend your application to fit their needs. So what are you waiting for get out there and code!

You can Download This Code Here