Building a Plugin Architecture with C#

Written By: Nathan Baker

- 06 Mar 2006 -
















Description: I will show how to use C# to create an extensible application using plugins.

  1. Introduction
  2. Beginnings
  3. Let's Write Some Code
  4. The Host Application
  5. Writing a Plugin in C#

The host application

Ok, so we've built ourselves a nice little framework here. Now it's time to build up a spiffy GUI and get to testing our code. Go back to your frmHost file and arrange the components on the form designer to be visually attractive and easy to use. And now that we know where we're going, let's add some more controls: create a groupbox, and inside that groupbox add seven labels. These labels will contain information about the plugin. Make three of them say Name, Version, and Author, and then make the others say something like "No Plugin Selected" (you could just make them blank, but blank labels tend to disappear and then it's hard to find them again). I also like to give my labels nice names rather than just labelN so that when I want to programmatically change them I don't have to keep looking up which label is what.

First, we want to select a folder to look for plugins in. Add the System.IO namespace to the global namespace (type "using System.IO;" up at the top) and add this code to your form:

private void btnChooseDir_Click(object sender, EventArgs e)
{
        if (fbdPluginFolder.ShowDialog() != DialogResult.Cancel) {
                SetPluginPath(fbdPluginFolder.SelectedPath);
        }
}
 
private void SetPluginPath(string path)
{
        if(Directory.Exists(path)){
                fbdPluginFolder.SelectedPath = txtPluginPath.Text = path;
                GetPlugins(path);
        }
}
 
private void GetPlugins(string Path)
{
        if (!Directory.Exists(Path)) {
                return;
        }
 
        Plugin curr = lstPlugins.SelectedItem as Plugin;
        lstPlugins.BeginUpdate();
        lstPlugins.Items.Clear();
 
        foreach (string f in Directory.GetFiles(Path)) {
                FileInfo fi = new FileInfo(f);
 
                if (fi.Extension.Equals(".dll")) {
                        lstPlugins.Items.Add(new Plugin(f));
                }
        }
        lstPlugins.EndUpdate();
 
        //Restore the previously selected plugin
        if (curr != null) {
                Plugin test;
                for (int i = 0; i < lstPlugins.Items.Count; ++i) {
                        test = lstPlugins.Items[i] as Plugin;
                        if (test.Path == curr.Path) {
                                lstPlugins.SelectedIndex = i;
                        }
                }
        }
}

Hmm, another huge code dump! Don't press F5 just yet, though, since we're still not all there. Note that if you're just copying and pasting my code, you will have to add btnChooseDir_Click to btnChooseDir's event handler list manually. You can do this by clicking the button in the form designer, and in the Properties window switching to Events (click the lightning bolt near the top of the pane) and going to the Click event. Click the down arrow and select btnChooseDir_Click from the list. Also note that you may have to change some of the object names if you don't like my naming conventions.

To be honest, this is all easy stuff. You're just selecting a directory, enumerating the files in that directory, and then adding those files to the list. Note that the call to lstPlugins.Items.Clear() means that you can only enumerate one directory at a time. You can remove that call to allow the old stuff to stick around.

Loading the plugin

Now, if you've been following along (and if you haven't, what are you still doing here?), you will have noticed that I've done pretty much everything but the most important part: the actual loading of the plugin. Well, isn't it about time we fixed that?

In your Plugin class (not the IPlugin interface, note!), add the System.Reflection namespace (using System.Reflection). Then add the following code:

public bool SetPlugin(string PluginFile)
{
        Assembly asm;
        PluginType pt = PluginType.Unknown;
        Type PluginClass = null;
 
        if (!File.Exists(PluginFile)) {
                return false;
        }
        asm = Assembly.LoadFile(PluginFile);
 
        if (asm != null) {
                myPath = PluginFile;
                foreach (Type type in asm.GetTypes()) {
                        if (type.IsAbstract) continue;
                        object[] attrs = type.GetCustomAttributes(typeof(PluginAttribute), true);
                        if (attrs.Length > 0) {
                                foreach (PluginAttribute pa in attrs) {
                                        pt = pa.Type;
                                }
                                PluginClass = type;
                                //To support multiple plugins in a single assembly, modify this
                        }
                }
                if (pt == PluginType.Unknown) {
                        return false;
                }
 
                //Get the plugin
                internalPlugin = Activator.CreateInstance(PluginClass) as IPlugin;
                myType = pt;
                return true;
        }
        return false;
}

In addition, we need to add a call to SetPlugin to the constructor (I bet you were wondering where myPath was set, huh?):

public Plugin(string Path)
{
        SetPlugin(Path);
}

Bang, that's the magic, folks! This is what turns this project from something a middle-schooler could do on his or her own into one that needs an online tutorial. The System.Reflection namespace allows you to look at your own metadata (hence the name). However, it also allows you to look at others' metadata, which is what we're using it for. Essentially, we go through each class in the .dll, look at their attributes, and load one that has the PluginAttribute set. Notice that this is why we have the PluginAttribute remember the type of the plugin. This makes it a lot easier. If we wanted to support multiple plugins per file (say, a math plugin AND a drawing plugin in the same .dll), we would need to do some fairly extensive modifications.

<< Previous

Next >>