Provide plug-in support in your own applications
A tutorial showing how to add plug-in support to your own applications.
Introduction
This tutorial will show how to provide plug-in support in your own applications using plain .NET technologies like interfaces and Reflection.
Background
I think you have already used an application providing plug-in support. The variety of such applications is pretty large - e.g., graphic programs (like GIMP, Adobe Photoshop), text editors (like Notepad++), etc. How could you achieve to provide such support, too - enabling other developers to extend the possibilities of your own applications with their own code?
The solution
To provide Plug-In support, we use basic technologies like interfaces and Reflection. At the end of this tutorial there will be a very brief sample application, to list and call the available interfaces.
Setting up the project
The first thing we have to do is to create a new project. I chose "Console Application" as type and set its name to "DBB.PluginsTest". Leave this project as is so far.
Now we need to create our interface that enables us to gain access to plug-ins and call the plug-in's methods. To achieve this, we set up another project in our solution, which is of type "Class Library", and in my case called "DBB.Plugins.Ext".
The third and last step, setting up the project, is creating our plug-in project. So we add a new project to our solution. Its type is also "Class Library" and I called it "TestPlugin1". For our testing purposes, we change the output path for this project to the output path of our "DBB.PluginsTest" folder - that assures our plug-in resides in our working directory and we do not have to copy it around for any of our tests. To change the properties, right click the "TestPlugin1" project, choose "Properties", and set the output path in the "Build" tab to the appropriate one.
Defining our plug-in interface
Because we do not know what exactly the plug-in does and its methods, properties, etc., we need to define a standard interface which is implemented by every plug-in that shall be used with our application. For our purposes, we create a very basic interface which just holds information about the plug-in's name, its version, and a method to run.
Create a new folder in the "DBB.Plugins.Ext" project called "Interfaces". In this folder, we create a new interface called
IPlugin
.
namespace DBB.Plugins.Interfaces
{
public interface IPlugin
{
/// <summary>
/// Gets or sets the plugin's name.
/// </summary>
string Name { get; }
/// <summary>
/// Gets or sets the plugin's version.
/// </summary>
string Version { get; }
/// <summary>
/// Runs the plugin's main method.
/// </summary>
/// <returns>Some object.</returns>
object Run();
}
}
Defining our dummy plug-in
After defining our default plug-in interface, we are going to define our dummy
plug-in. To do so we have to add a reference to "DBB.Plugin.Ext" - the library that holds our
plug-in interface. After adding this reference,
the interface IPlugin
is available in the plug-in we are going to implement.
Let's add a new class called TestPlugin11
to the "TestPlugin1" project. This class implements the IPlugin
interface and looks as follows:
using DBB.Plugins.Interfaces;
namespace TestPlugin1
{
public class TestPlugin11 : IPlugin
{
/// <summary>
/// Does something.
/// </summary>
/// <returns>Some string.</returns>
private string DoSomething()
{
return string.Format("{0} - Version: {1} -> {2}", Name, Version, "DoSomething()");
}
#region IPlugin members.
/// <summary>
/// Gets the plugin's name.
/// </summary>
public string Name
{
get { return "TestPlugin1.1"; }
}
/// <summary>
/// Gets the plugin's version.
/// </summary>
public string Version
{
get { return "1.0.0.0"; }
}
/// <summary>
/// Runs the plugin's main method.
/// </summary>
/// <returns>Some object.</returns>
public object Run()
{
return DoSomething();
}
#endregion
}
}
As you can see, IPlugin
has been implemented and the DoSomething()
method returns some information about the plug-in. Of course it is also possible to implement
a much more complex function.
Fill the test project with life
At this point, our interface is defined and our dummy plug-in is implemented. What we still need is some possibility to find available
plug-ins
and call their functionality (the Run()
method).
First, let's add a new folder to the "DBB.PluginsTest" project; called "PluginsHandling"
and a new class PluginUtils
within this new folder. Because we want to access our IPlugin
interface from our test project, we have to add a reference to the "DBB.Plugins.Ext" project
in the "DBB.PluginsTest" project.
The whole plug-in handling is done within just two methods:
- Retrieving .dll files, which might contain plug-in functionality.
- Retrieving valid plug-ins from found in .dll files.
First we implement a method that retrieves all ".dll" files of a specified path.
/// <summary>
/// Gets a list of Dll files within a specified path.
/// </summary>
/// <param name="path">Path to retrieve Dll files in.</param>
/// <returns>List of strings with file names.</returns>
public static List<string> GetDllList(string path)
{
return new List<String>(Directory.GetFiles(path, "*.dll"));
}
After that the interesting part needs to be implemented: Reading the .dll files and checking for valid plug-in functionality.
This check is done via Reflection technologies and looks as follows:
/// <summary>
/// Gets a list of IPlugin objects.
/// </summary>
/// <param name="dllFiles">List of Dll files to check for plugin capabilities.</param>
/// <returns>List of IPlugin objects.</returns>
public static List<IPlugin> GetPlugins(List<string> dllFiles)
{
List<IPlugin> retVal = new List<IPlugin>();
foreach (string dll in dllFiles)
{
try
{
// Load dll.
Assembly assembly = Assembly.LoadFile(dll);
// Get class names.
Type[] types = assembly.GetTypes();
// Add plugin classes to plugin list.
foreach (Type cls in types)
{
if (cls.IsPublic)
{
// Get classe's implemented interfaces.
Type[] interfaces = cls.GetInterfaces();
foreach (Type iface in interfaces)
{
// Is current interface IPlugin?
if (cls.GetInterface(iface.FullName) == typeof(IPlugin))
retVal.Add(assembly.CreateInstance(cls.FullName) as IPlugin);
}
}
}
}
catch (Exception ex)
{
throw ex;
}
}
return retVal;
}
What are we doing here?
First we try to load each .dll file (assembly) the provided dllFiles
list contains. Then we try to retrieve the
class names within the single assembly and check whether the class is public. If so, we try to retrieve the interfaces the class is implementing.
If an IPlugin
interface is implemented, we add an instance of the current class to the list of valid
plug-ins.
How to use the code?
Actually we neglected the "Program.cs" so far. We implemented everything we need to handle plug-ins but are not able to test it. Let's change this fact and open the file "Program.cs".
To our main
method we add some code to retrieve the available ".dll" files and available
plug-ins within them.
static void Main(string[] args)
{
// Get list of Dll files within current directory.
List<string> dllFiles = PluginUtils.GetDllList(Directory.GetCurrentDirectory());
// Get plugins within found Dll files.
List<IPlugin> plugins = PluginUtils.GetPlugins(dllFiles);
}
Here we are retrieving all ".dll" files within the current working directory and trying to get valid plug-ins within these.
To list the found plug-ins we implement another method within the "Program.cs" file:
/// <summary>
/// Shows a list of found plugins.
/// </summary>
/// <param name="plugins">List of IPlugin objects.</param>
private static void ShowPluginList(List<IPlugin> plugins)
{
Console.WriteLine("Found plugins");
Console.WriteLine("-----------------------------------------------------");
int i = 0;
foreach (IPlugin plugin in plugins)
Console.WriteLine(string.Format("{0,2}) {1}", ++i, plugin.Name));
Console.WriteLine(" x) Exit");
}
Here we are just building a small overview about found plug-ins. Now let's make this overview to a small menu, allowing us to call the
appropriate plug-in. For this purpose, we move our focus again to the main
method, to which the following code is added after retrieving the valid
plug-ins.
...
string key = string.Empty;
// Show menu and run selected plugin.
while (key.Trim().ToLower() != "x")
{
// Show a list of found plugins.
ShowPluginList(plugins);
Console.Write("--> ");
key = Console.ReadLine();
int pk;
int.TryParse(key, out pk);
Console.WriteLine();
if (pk != 0)
Console.WriteLine(((IPlugin)plugins[pk - 1]).Run());
Console.WriteLine();
}
...
I think the code is quite self explaining. Until another value than "x" is read from the keyboard, we call the
Run()
method of the list entry and print its result value to the screen.
Conclusion
If you have everything set up and implemented correctly, you should now be able to run your solution and get the following result where you can choose the plug-in to run. (Note: The screenshot shows a second implemented plug-in, which is not part of the sources presented in this tutorial, but part of the sources available to download.)
Points of Interest
- Is there really a need of "DBB.Plugins.Ext"?
- Why is the plug-in handling implemented in "DBB.PluginsTest"?
- Is the plug-in support safe?
- What do I have to provide to a developer who wants to create a plug-in?
Yes, there is! This .dll file is the window to the world and the world's window to our application. You can implement the interface
in both projects but the compiler will not know to which type to cast the object. So you will run into a CastTypeException
if you do not use the way with the extra .dll file.
Well, you can also implement the whole handling in "DBB.Plugins.Ext". But then you enable plug-ins to have plug-pns to have plug-pns, ...
No! Plug-in support is unsafe! You cannot control what a plug-in does. It might be named like "Rename files" and format your harddisk after it has been called.
Just provide the developer the "DBB.Plugins.Ext" .dll file and the description of your IPlugin
interface.
History
- May 03, 2012 - Initial version.