As you may or may not know, Visual Studio has many great addons that can be found across the web. Basically, Microsoft has an API that developers can code against, and then "plugin" their addons into Visual Studio. How is this accomplished? How does it know how to load the code I wrote into Visual Studio? The basic idea behind this is a simple Plugin System that can be accomplished using Reflection. I'll demonstrate the basic premise here to give you a simple understanding of how this all works. This post will be more of a tutorial kind of deal so follow along with me as I go through the steps and I explain them.
Before we do any code, let me explain the basic idea of what we'll do. We're going to create an Interface that we'll call IPluginInterface which will have one simple get property called "Name". All "plugins" will have to implement this interface. At runtime, we will then look in a specific folder for any dll files that have types that implement this interface. If it does, then we know it's a plugin, and we can load it up into our app. May not make sense now, but bear with me. It'll all make sense once we start coding.
OK, fire up Visual Studio and create a Console Application callaed "AssemblyLoadingDemo". Change the default namespace to: SetFocusDemo.Reflection.Demo. You should now see this:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5
6 namespace SetFocusDemo.Reflection.Demo
7 {
8 class Program
9 {
10 static void Main(string[] args)
11 {
12
13 }
14 }
15 }
OK, for now we won't do anything in this project. Now Click on File -> New -> Project and select "Class Library". Call it "PluginInterface". Get rid of the default Class1.cs, and add a class called "IPluginInterface.cs". Then, add this code to the interface:
namespace SetFocusDemo.Reflection.Contract
{
///
/// This interface will need to be implemented by any class that wants to be a plugin
/// in our Demo Plugin project.
///
public interface IPluginInterface
{
string Name { get; }
}
}
Simple interface, doesn't do much really except it has the Name getter like we talked about. In a real life scenario, you would probably have multiple interfaces with multiple methods and properties, but again this is just a demo, so one is enough. Also, please note that I put this in the "SetFocus.Reflection.Contract" namespace.
OK, now, let's actually create a few "Plugins". Basically they'll be classes that will implement this interface.First, let's create another Project. Click File -> New -> Project and create a Class Library called "Plugin Library". Then, right click on "References" and click "Add Reference". Select the Projects tab on top and click select the "Plugin Interface" assembly. Once that's done, remove the default "Class1.cs" and add a new class called "CustomPlugin1". Then, add this code:
namespace PluginLibrary
{public class CustomPlugin1 : IPluginInterfaceusing SetFocusDemo.Reflection.Contract;
{
public string Name
{
get { return "Custom Plugin 1"; }
}
}
}
namespace PluginLibrary
{ using SetFocusDemo.Reflection.Contract;
public class AnotherPlugin : IPluginInterface
{
public string Name
{
get { return "Another Plugin"; }
}
}
}
Same thing as before really. Another class that implements the same interface. Now, click on the Build menu and click "Build PluginLibrary". Once that's done, right click on the "Plugin Library" project on the right side in solution explorer and click on "Open Folder in Windows Explorer". Then, go to the Bin -> Debug folder and copy the "PluginLibrary.dll" file. Then, open your MyDocuments folder and paste it in there.
OK, we finally have all the code set up to actually write our Plugin Loader. This is where the Reflection magic will happen. Go back to the first project that we called "AssemlyLoadingDemo". First, add a reference to the "Plugin Interface" project and add the using statement on top: "using SetFocusDemo.Reflection.Contract;" Add a new class and call it "PluginLoader.cs". This class will have one static method that will return an array of all the IPluginInterface objects it found and created. Here's the actual code of the class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SetFocusDemo.Reflection.Contract;
using System.IO;
using System.Reflection;
namespace SetFocusDemo.Reflection.Loader
{
public class PluginLoader
{
public static IPluginInterface[] GetPlugins(string directory)
{
if (String.IsNullOrEmpty(directory)) { return null; } //sanity check
DirectoryInfo info = new DirectoryInfo(directory);
if (!info.Exists) { return null; } //make sure directory exists
List<IPluginInterface> plugins = new List<IPluginInterface>();
foreach (FileInfo file in info.GetFiles("*.dll")) //loop through all dll files in directory
{
//using Reflection, load Assembly into memory from disk
Assembly currentAssembly = Assembly.LoadFile(file.FullName);
//Type discovery to find the type we're looking for which is IPluginInterface
foreach (Type type in currentAssembly.GetTypes())
{
if(!type.ImplementsInterface(typeof(IPluginInterface)))
{
continue;
}
//Create instance of class that implements IPluginInterface and cast it to type
//IPluginInterface and add it to our list
IPluginInterface plugin = (IPluginInterface)Activator.CreateInstance(type);
plugins.Add(plugin);
}
}
return plugins.ToArray();
}
}
}
There's alot of code here, I know and I'll do my best to explain. Before I do that though, two things to note. First, you'll see a using statment on top to System.Reflection. Secondly, you'll see this line of code: type.ImplementsInterface(typeof(IPluginInterface)) but that's an extension method I wrote. (For more on extension methods, see my blog post about them here.) Right click on the project and add a class called "TypeExtensions". Here's the code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SetFocusDemo.Reflection.Loader
{
public static class TypeExtensions
{
public static bool ImplementsInterface(this Type type, Type interfaceType)
{
return type.GetInterfaces().Contains(interfaceType);
}
}
}
Simple method that checks to see if a given type implements a specific interface.
OK, now we can go back and explain what's going on here.
The GetPlugins method takes a directory name as it's parameter indicating where we want to check for plugins. First we just do some sanity checking to make sure we don't get any Null Reference Exceptions:
if (String.IsNullOrEmpty(directory)) { return null; } //sanity check
DirectoryInfo info = new DirectoryInfo(directory);
if (!info.Exists) { return null; } //make sure directory exists
Now we know our "info" object points to the directory we want to look for Plugins.
List<IPluginInterface> plugins = new List<IPluginInterface>();
Here we just new up a List that will be used to add the plugins found and returned from the method.
foreach (FileInfo file in info.GetFiles("*.dll")) //loop through all dll files in directory
{
//using Reflection, load Assembly into memory from disk
Assembly currentAssembly = Assembly.LoadFile(file.FullName);
//Type discovery to find the type we're looking for which is IPluginInterface
foreach (Type type in currentAssembly.GetTypes())
{
if(!type.ImplementsInterface(typeof(IPluginInterface)))
{
continue;
}
//Create instance of class that implements IPluginInterface and cast it to type
//IPluginInterface and add it to our list
IPluginInterface plugin = (IPluginInterface)Activator.CreateInstance(type);
plugins.Add(plugin);
Finally we clost up and return the List as an array:
return plugins.ToArray();
Ok, we now have all the code to give this a wirl. Go back to your Program.cs and in the main method add this code:
static void Main(string[] args)
{
//load plugins from MyDocuments. This is just a demo, in a real scenario, you'd use some
//specific custom folder
IPluginInterface[] plugins = PluginLoader.GetPlugins(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
foreach (IPluginInterface plugin in plugins)
{
Console.WriteLine("Plugin: \"{0}\" was found and loaded.", plugin.Name);
}
Console.ReadLine();
}
Basically, call our static method that loads up the plugins, loop through them and print out the name. Here's the output you should see:
Plugin: "Custom Plugin 1" was found and loaded.
Plugin: "Another Plugin" was found and loaded.
What happened here? Basically, we copied our "Plugins" to our MyDocuments folder. When our PluginLoader went to look for Assemblies, it looked in the MyDocuments folder, and found our PluginLibrary.dll. It then loaded up the assembly and looked through the Types found there. It found "CustomPlugin1" and "AnotherPlugin". Then, it actually created instances of each of those classes and cast them as IPluginInterface. This is key! At design time, we have NO idea what the type we'll be. Any developer coding against our IPluginInterface API can create their own class that implements IPluginInterface. We have no clue what his class will be. What we DO know is that it will be of type IPluginInterface. Therefore, when we create an instance of it using Activator.CreateInstance, and cast it to the only type we know of which is IPluginInterface. Then, we can call the methods on our interface which in our cast is the Property "Name".
This post turned out to be alot longer than I thought. There's alot happening here, so if you have any questions, seriosuly, email me at a.friedman07@gmail.com or post a comment here.
No comments:
Post a Comment