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 Interface
s 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 (Name
, Description
, Author
, Version
), an IPluginHost
type property, a UserControl
property, and two void
s: 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 that IPlugin
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
{
public class Global
{
public Global(){}
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 PluginServices
object. 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 the PluginServices
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)
{
colAvailablePlugins.Clear();
foreach (string fileOn in Directory.GetFiles(Path))
{
FileInfo file = new FileInfo(fileOn);
if (file.Extension.Equals(".dll"))
{
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:
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’s public
and accessible to us, as well as not an abstract
class. Here is the next chunk of code:
Type typeInterface = pluginType.GetInterface("PluginInterface.IPlugin", true);
if (typeInterface != null)
{
Types.AvailablePlugin newPlugin = new Types.AvailablePlugin();
newPlugin.AssemblyPath = FileName;
newPlugin.Instance = (IPlugin)
Activator.CreateInstance(pluginAssembly.GetType(pluginType.ToString()));
newPlugin.Instance.Host = this;
newPlugin.Instance.Initialize();
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 IPlugin
interface. If the Type
does actually implement the IPlugin
interface, typeInterface
will not be null
. 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 the Host
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)
{
System.Windows.Forms.Form newForm = null;
frmFeedback newFeedbackForm = new frmFeedback();
newFeedbackForm.PluginAuthor = "By: " + Plugin.Author;
newFeedbackForm.PluginDesc = Plugin.Description;
newFeedbackForm.PluginName = Plugin.Name;
newFeedbackForm.PluginVersion = Plugin.Version;
newFeedbackForm.Feedback = Feedback;
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 the PluginInterfaces
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 the UserControl
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
and IPluginHost
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 and UserControl
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!
History
February 26th, 2004 - Submitted article.