Friday, 5 February 2016

Plug-ins in C#

Plugins in C#!

Introduction:

First things first: I wrote this article mainly because I’ve found a complete lack of comprehensive information on the subject. Sure there are projects out there that show you how to make a simple plug-in that lets you do something dumb like feed it two numbers, and then one plug-in may add the two numbers together and return the result, while the other plug-in may multiply them instead. This is where I began, and is not bad reading as a prep tutorial to this tutorial. It might not be a bad idea to understand those tutorials first. However, when I began to search for how to bring GUIs into the plug-ins, I was completely lost. My aim is to show you how to do this, and provide some clear explanations. This tutorial will not be a step by step guide to making the program, as if you have the tutorial, you should also have the source code for the Tutorial project. I feel it unnecessary to go through this all, so instead I will only be covering some explanations on the code that is tricky.

Tutorial:

Notice that we have a Solution with 6 different projects in it. The first project is the Plugin Application Tester thingy. I like to refer to it as the Host, since it is the application that your plug-ins will be plugging-in to. This of course is the heart and soul of the whole Plug-in get-up. The plug-ins themselves are really not that complex as you will see.
But before I go into the Host Application, let’s talk about the PluginInterface project. If you are unfamiliar with interfaces, please go find a tutorial and familiarize yourself with them. In a nutshell, they are ‘outlines’ of what properties and methods a Class that inherits a certain interface should have in them. There is very little code in this project. In fact, the only code we’re worried about is this:
using System;

namespace PluginInterface
{
    public interface IPlugin
    {
        IPluginHost Host {get;set;}

        string Name {get;}
        string Description {get;}
        string Author {get;}
        string Version {get;}

        System.Windows.Forms.UserControl MainInterface {get;}

        void Initialize();
        void Dispose();

    }

    public interface IPluginHost
    {
        void Feedback(string Feedback, IPlugin Plugin);
    }
}
In this code, we have 2 Interfaces declared. These are the legends, the roadmaps, the keys to what a class inheriting these interfaces should do. For example, if we were to declare a new class in code and have it inherit IPlugin interface, the class would have to employ four readonly string properties (NameDescriptionAuthorVersion), an IPluginHost type property, a UserControl property, and two voids: one called Initialize, the other called Dispose. Only if a class has all of that as said above, can it legally inherit the IPlugin Interface. This of course makes sense if you think about it. If we’re going to make plug-ins, we need establish some ground rules. Just think of what would happen if we tried calling a method in a plug-in that didn’t exist! This way, we establish a set of guidelines that every plug-in must follow, and we can predict just what the plug-in is going to be capable of doing. When all is said and done, we have 2 interfaces: IPlugin and IPluginHost. It should be clear thatIPlugin is the interface that all Plug-ins will have to inherit. However, we also will make our Plug-in host inherit the IPluginHost interface. This way, we can send the Plug-in a reference to the host later on. This will allow the Plug-in some access to things of its host. When this project is compiled, it produces PluginInterface.dll file. We made this in its very own DLL for a reason. Now we have a common roadmap to share between plug-ins and the plug-in host that contains definitions for each object.
Next up, we have the Host project. This one is a bit more confusing. First of all, let’s get this cleared out of the way. I used a technique that may be a bit confusing at first for some, but once scaled up, can make things a bit simpler later on.
using System;

namespace Host
{
    /// <summary>
    /// Holds A static instance of global program shtuff
    /// </summary>
    public class Global
    {
        public Global(){} //Constructor

        //What have we done here?
        public static Host.PluginServices Plugins = new PluginServices();

    }
}
The above code is a trick I learned a little while ago. I have a class called Global. Inside this class is a static public object. This object in this case happens to hold an instance of the PluginServicesobject. All that this little trick does is makes sort of a global instance of the (PluginServices) object that can easily be accessed from the entire application. This is instead of declaring an object and making a new instance in the Main form of the host application. Now, I can access the instance of thePluginServices object anywhere in the program by typing Global.Plugins.! It’s that simple!
The next bit of code I will introduce is the code that actually finds any compatible plug-in files in a certain folder, and adds them to a collection of AvailablePlugins:
public void FindPlugins(string Path)
{
    //First empty the collection, we're reloading them all
    colAvailablePlugins.Clear();

    //Go through all the files in the plugin directory
    foreach (string fileOn in Directory.GetFiles(Path))
    {
        FileInfo file = new FileInfo(fileOn);

        //Preliminary check, must be .dll
        if (file.Extension.Equals(".dll"))
        {
            //Add the 'plugin'
            this.AddPlugin(fileOn);
        }
    }
}
This is a very simple method that accepts a path to look for plug-ins in. For example, you could tell it to look in your application’s Plugins folder. All it does is loop through all the files in the given directory, and check to see if the extension is of .dll type. Now you may be wondering if that’s enough to check for. What if someone renamed some random file as .dll and stuck it in? We’ll get to that later.
You may have noticed in the last method, a call to another method AddPlugin(). I’m not going to paste the whole function in here, as that would be entirely redundant, but I will explain what’s going on in a few spots. First of all, we’re declaring a new Assembly type, and loading the passed filename into it:
//Create a new assembly from the plugin file we're adding..
Assembly pluginAssembly = Assembly.LoadFrom(FileName);
Next, we’re iterating through all the different types that the particular plug-in contains. It would be wise to note at this time, that you may want to put in some extra error catching around this area. I would imagine if you tried loading an Assembly that isn’t a proper assembly, and getting its types, you may run into some problems. But we’ll leave that for you to play with. The next few if statements are just checking some attributes of the given type found in the assembly. We want to make sure it’spublic and accessible to us, as well as not an abstract class. Here is the next chunk of code:
//Gets a type object of the interface we need the plugins to match
Type typeInterface = pluginType.GetInterface("PluginInterface.IPlugin", true);

//Make sure the interface we want to use actually exists
if (typeInterface != null)
{
    //Create a new available plugin since the type implements the 
    //IPlugin interface
    Types.AvailablePlugin newPlugin = new Types.AvailablePlugin();

    //Set the filename where we found it
    newPlugin.AssemblyPath = FileName;

    //Create a new instance and store the instance 
    //in the collection for later use
    //We could change this later on to not load an instance.. 
    //we have 2 options
    //1- Make one instance, and use it whenever we need it.. 
    //                           it's always there
    //2- Don't make an instance, and instead make an instance 
    //whenever we use it, then close it
    //For now we'll just make an instance of all the plugins
    newPlugin.Instance = (IPlugin)
     Activator.CreateInstance(pluginAssembly.GetType(pluginType.ToString()));

    //Set the Plugin's host to this class which inherited IPluginHost
    newPlugin.Instance.Host = this;

    //Call the initialization sub of the plugin
    newPlugin.Instance.Initialize();

    //Add the new plugin to our collection here
    this.colAvailablePlugins.Add(newPlugin);
}
This bit of code is the heart of the whole plug-in project. First, we declare a new Type object, and make it the type of our IPlugin interface. Notice here, we’re calling the GetInterface() method. What this does is tries to sort of cast the Type that we are on in the for loop to the IPlugininterface. If the Type does actually implement the IPlugin interface, typeInterface will not benull. That’s really the hardest step. Next, we’re just making a new instance of the class we created ‘AvailablePlugin’. We set the AssemblyPath property. On the next line, we’re doing something else. The AvailablePlugin type has a property called Instance. This is where we will be storing the actual instance of the loaded plug-in, ready to use. Now, for the purpose of this tutorial, I tried to keep things easier. I decided that my program would simply load an instance of every plug-in it finds and have it ready for use, regardless of whether or not the plug-in will actually be used. There is another way to do this as well. What if you don’t want to use all the plug-ins? It’s a bit of a waste of memory to load them all up if you’re not going to use them all. You could implement some sort of Loading and Unloading of plug-ins if you choose to go this route. Like I said though, that is beyond the scope of this article.
So, we create a new instance of the plug-in with the following line:
newPlugin.Instance = (IPlugin)
   Activator.CreateInstance(pluginAssembly.GetType(pluginType.ToString()));
After this is done, we can use the new instance of the plug-in, and set its Host property. Since the class that we are working in happens to inherit the IPluginHost interface, we can conveniently set theHost property to this. Finally, we call the Initialize() method of the plug-in to alert it to do anything it needs to start, and then we add it to the collection of AvailablePlugins.
The only other bit of code I think we should cover is the FormFeedback() function implemented by the PluginServices class:
public void Feedback(string Feedback, IPlugin Plugin)
{
    //This sub makes a new feedback form and fills it out
    //With the appropriate information
    //This method can be called from the actual plugin with its Host Property

    System.Windows.Forms.Form newForm = null;
    frmFeedback newFeedbackForm = new frmFeedback();

    //Here we set the frmFeedback's properties that i made custom
    newFeedbackForm.PluginAuthor = "By: " + Plugin.Author;
    newFeedbackForm.PluginDesc = Plugin.Description;
    newFeedbackForm.PluginName = Plugin.Name;
    newFeedbackForm.PluginVersion = Plugin.Version;
    newFeedbackForm.Feedback = Feedback;

    //We also made a Form object to hold the frmFeedback instance
    //If we were to declare if not as  frmFeedback object at first,
    //We wouldn't have access to the properties we need on it
    newForm = newFeedbackForm;
    newForm.ShowDialog();

    newFeedbackForm = null;
    newForm = null;

}
This is really simple actually. The whole idea of the IPluginHost interface is to allow some communication between the main application that runs the plug-ins, and the plug-ins themselves. In this example, we didn’t take advantage of this idea to a great extent, however in your own applications, this idea can become extremely powerful. Think about it. You could expose a lot of functionality of your main program. For example, let's say you were making an MP3 player, you could make the IPluginHost interface have methods such as PlayMp3()Stop()Previous()Next(),SetVolume(), etc. This would allow your plug-ins to take some control over the main program. Hopefully, this idea makes sense to you. In our example here, all we’ve done is incorporated a method that plug-ins can call which displays a new form with a string that the plug-in passed along, as well as some information on the plug-in that called the method.
OK, so we talked about the actual host application for the plug-ins, but we have yet to talk about the plug-ins themselves. The plug-ins themselves are not that complicated. All you have to do to make one is create a new Class Library project, and make a class that inherits the IPlugin interface. Remember, you will need to add a reference to the project to the PluginInterfaces project that our interfaces reside in. Also, another tip: when you add the reference, make sure the CopyLocal property of the reference is set to false! Otherwise, you will have some unexplained problems. The only place where the CopyLocal property should be true is on the Host application’s reference to thePluginInterfaces project. This really makes sense if you think about it, since the Host will always have the PluginInterfaces.Dll file. The plug-ins just use it from the Host program.
I’ll let you take a look at the actual plug-ins yourself. Plug-ins 1 through 3 are all made pretty much the same. They all have a Class which inherits the IPlugin interface and implements all its methods and properties. The plug-ins also contain a UserControl class in them. This is the GUI for the plug-in. It is exposed as a property in the IPlugin interface (MainInterface). This allows the host to add theUserControl to a panel or something to expose a GUI to the plug-in.
Notice that in Plugin3, in the UserControl itself, notice we have properties of IPlugin andIPluginHost types. This allows the UserControl access to the Plug-in Host’s methods (in this case,ShowFeedback(), which is the whole point of this plug-in), and the Plug-in’s methods itself.
Finally, in Plugin4, we demonstrate that you don’t need to have a separate Plugin class andUserControl class for a plug-in. We created a UserControl that also inherits the IPlugin interface. We’ve combined two classes into one.

Conclusion:

And that’s about it for plug-ins. Once you’ve grasped the concept of Interfaces, and have understood the code for actually initializing instances of plug-ins, creating an actual plug-in is not a very difficult task. I will leave you to explore the rest of the code by yourself. Hopefully you have learned something from this tutorial, and can successfully implement plug-ins into your own programs!

No comments:

Post a Comment