Add run-time functionality to your application by providing a plug-in mechanism
· Download source files - 102 Kb
· Download demo project - 10 Kb
Issues covered:
o Using the Activator Class
o Using custom configuration sections by implementing IConfigurationSectionHandler
o Designing a simple Plug-in architecture
What you’ll need:
· VS.Net 2003 (If you have 2002, use this utility to convert the files to the previous format)
Why do we need a plug-in framework for our application?
People usually add plug-in support in their applications for the following reasons:
· We want to allow our application to be extended with more functionality without the need to re-compile and distribute it to customers
· We need to add functionality on site
· We need to fix a bug on site
· The business rules for the application change frequently, or new rules are added frequently
Case Study
In our case study, we will build a very simple text editor, composed of only one form. The only thing that text editor can do is display text in a single textbox in the middle of the form. Once this application is ready, we will create a simple plug-in which will be added to the application. That plug-in will be able to read the text currently in the textbox , parse it for valid email addresses, and return a string containing only those emails. We will then put this text inside the text box.
As you can see, there are a number of “unknowns” in our case study:
- How do we find the plug-in from within the application?
- How does the plug-in know what text is in the text box?
- How do we activate this plug-in?
We will answer all of these questions when we encounter them as we build the solution.
Step 1 – Create a simple text editor
OK. I won’t bore you with the details of this. It’s all in the source code download. Just a simple form showing a lump of text.
I’ll assume from this moment that you have created this simple application.
Step 2 – Create the Plug-in SDK
Now that we have an application, we want it to be able to talk with external plug-ins. How do we make this happen?
The solution is for the application to work against a published interface – a set of public members and methods which will be implemented by all custom plug-ins.
We’ll call this interface IPlugin. From now on, any developer that would like to create a plug-in for our application will have to implement this interface.
This interface will be located at a shared library, which both our application and any custom plug-ins will reference.
Let’s define this interface then. We need very little data from our simple plug-in - its name , and a method which would instruct it to perform a generic action based upon the data in our application.
public interface IPlugin
{
string Name{get;}
void PerformAction(IPluginContext context);
}
The code is pretty straight forward, but I’ll explain why I’m sending an IPluginContext interface to the PerformAction. The reason I’m sending an interface rather than just a string is because I want to allow more flexibility in the matter of what object will I be able to send. Currently, this interface is very simple:
public interface IPluginContext
{
string CurrentDocumentText{get;set;}
}
Now, all I have to do is implement this interface in one or more objects, and send this to any plug-in to receive a result. In the future this will allow me to chane the string of not just a textbox, but any object I like.
Step 3 – Creating our custom Plug-in
All we have to do now is :
· create a separate class library object
· Create a class that implements the IPlugin Interface
· Compile that class and place it in the same folder as out main application
Here’s the EmailPlugin Class, in full:
public class EmailPlugin:IPlugin
{
public EmailPlugin()
{
}
/// The single point of entry to our plugin
/// Acepts an IPluginContext object
/// which holds the current
/// context of the running editor.
/// It then parses the text found inside the editor
/// and changes it to reflect any
/// email addresses that are found.
public void PerformAction(IPluginContext context)
{
context.CurrentDocumentText=
ParseEmails(context.CurrentDocumentText);
}
/// The name of the plugin as it will appear
/// under the editor's "Plugins" menu
public string Name
{
get
{
return "Email Parsing Plugin";
}
}
/// Parse the given string for any emails using the Regex Class
/// and return a string containing only email addresses
private string ParseEmails(string text)
{
const string emailPattern=
@"\w+@\w+\.\w+((\.\w+)*)?";
MatchCollection emails = Regex.Matches(text,emailPattern,RegexOptions.IgnoreCase);
StringBuilder emailString = new StringBuilder();
foreach(Match email in emails)
{
emailString.Append(
email.Value + Environment.NewLine);
}
return emailString.ToString();
}
}
Step 4 – Letting our application know about the new plug-in
Once we have compile out plug-in, how do we let our application know about it?
The solution is simple :
· Create an application configuration file
· Create a section in the config file that lists all the available plugins
· Create a parser for this config section
OK. To take care of step one, Just add an XML file to the Main application.
Tip: Name this file App.Config. If you do that, every time you build your application VS.NET will automatically copy this file into the build output folder and rename it to
Now, We want the plug-in developer to easily add an entry in the Config file to publish each plug-in he has created.
Here’s how the Config file should look:
<configuration>
<configSections>
<section name="plugins"
type="Royo.PluggableApp.PluginSectionHandler, PluggableApp"
/>
configSections>
<plugins>
<plugin type="Royo.Plugins.Custom.EmailPlugin, CustomPlugin" />
plugins>
<configuration>
Notice the configSections Tag. We tell the application Configuration settings that we have an unidentifies section in this config file, but that we have a parser for this section. This parser resides in the class “Royo.PluggableApp.PluginSectionHandler”, which which in an assembly named “PluggableApp”.
I’ll show you the code for this class in the next.
Next, we have the “Plugins” section of the config file, which lists , for every plug-in, the class name and the assembly name in which it resides.
We will use this information when we instantiate the plug-in, later on.
OK. Once the Config file is done, we basically have finished one end of the circle. The plug-in is ready to rock, and has published itself to all the necessary channels. All we have left to do now is to allow our application to read in this information, and instantiate the published plugins according to this info.
Step 5 – Parse the config file using IConfigurationSectionHandler
In order to parse out the plugins that are found within the Config file of our application, The framework provides a very simple mechanism that enables us to register a specific class as a "handler" for a specific portion in our config file. We must have a handler for any portion in the file that is not automatically parsed by the framework, otherwise we get a "ConfigurationException" thrown.
In order to provide the class that parses the "plugins" section, all we need to do is to implement the System.Configuration.IConfigurationSectionHandler interface.
The interface itself is very simple:
public interface IConfigurationSectionHandler
{
public object Create(object parent, object configContext, System.Xml.XmlNode section);
}
All we have to do is override the "create" method in our custom class, and parse the XML node which is provided to us. This xml node, in our case, will be the "Plugins" XML node. Once we have that, we have all the information we need in order instantiate the plugins for our application.
Our custom class must provide a default constructor, since it is instantiated automatically by the framework at run time, and than the "Create" method is called on it.
Here's the code for the Plugins Section Handler class:
public class PluginSectionHandler:IConfigurationSectionHandler
{
public PluginSectionHandler()
{
}
/// Iterate through all the child nodes
/// of the XMLNode that was passed in and create instances
/// of the specified Types by reading the attribite values of the nodes
/// we use a try/Catch here because some of the nodes
/// might contain an invalid reference to a plugin type
public object Create(object parent, object configContext, System.Xml.XmlNode section)
{
PluginCollection plugins = new PluginCollection();
foreach(XmlNode node in section.ChildNodes)
{
//Code goes here to instantiate
//and invoke the plugins
.
.
.
}
return plugins;
}
}
}
As you can see in the config file mentioned earlier, We provide the data the framework needs in order to handle the plugins section usinf the "configSection" tag prior to the actual "plugins" tags.
<configuration>
<configSections>
<section name="plugins"
type="Royo.PluggableApp.PluginSectionHandler, PluggableApp"
/>
<configSections>
.
.
.
Notice how we specify the class ; The string is composed of two sections : The full name of the class(including encapsulating namespaces), comma, The name of the Assembly in which this class is located. This is all the framework needs in order to instantiate a class, and unsurprisingly, this is exactly the information we require for any plugins to register for our application.
Instantiating and invoking the plugins
Let's see how we actually instantiate an instance of a plug-in given the following string:
String ClassName = "Royo.Plugins.MyCustomPlugin, MyCustomPlugin"
IPlugin plugin = (IPlugin )Activator.CreateInstance(Type.GetType(ClassName));
Let's explain what's happening here.
Since our application does not a direct reference to the assembly of the custom plug-in, we use the System.Activator class . Activator is a special kind of class which is able to create instances of object given any number of specific parameters. It can even create Com Instances of objects and return them. If you have ever coded in ASP or VB, you remember the "CreateObject()" function which was used to instantiate and return objects based on the CLSID of a class. Activator operates on the same idea, yet uses different arguments, and returns a System.Object instance, not a variant….
In this call to Activator, I pass in as a parameter the Type which I want to instantiate. I uses the Type.GetType() method to return an instance of a Type which matches the Type of the plug-in. Notice that the Type.GetType() method accepts as a parameter exactly the string which was put inside the "plugins" tag, which describes the name of the class and the assembly it resides in.
Once I have an instance of the plug-in, I cast it to an IPlugin interface, and put it inside my plug-in object. An Try-Catch block must be put on this line, since we cannot be sure that the plug-in that is described there actually exists, or does in fact support the IPlugin interface we need.
Once we have the instance of the plug-in, we add it to the ArrayList of our application plugins, and move on to the next XML node.
Here’s the code from our application:
public object Create(object parent, object configContext, System.Xml.XmlNode section)
{
//Derived from CollectionBase
PluginCollection plugins = new PluginCollection();
foreach(XmlNode node in section.ChildNodes)
{
try
{
//Use the Activator class's 'CreateInstance' method
//to try and create an instance of the plugin by
//passing in the type name specified in the attribute value
object plugObject = Activator.CreateInstance(Type.GetType(node.Attributes["type"].Value));
//Cast this to an IPlugin interface and add to the collection
IPlugin plugin = (IPlugin)plugObject;
plugins.Add(plugin);
}
catch(Exception e)
{
//Catch any exceptions
//but continue iterating for more plugins
}
}
return plugins;
}
Invoking the plugins
After all this work is done, we can now use the plugins.One more thing is missing, though. Remember that the IPlugin.PerformAction() requires an argument of type IPluginContext, which holds all the necessary data for the Plug-in to do its work. We'll implement a simple class which implements this interface, which we send to the PerformAction() method whenever we call a plug-in. Here's the code for the class:
public interface IPluginContext
{
string CurrentDocumentText{get;set;}
}
public class EditorContext:IPluginContext
{
private string m_CurrentText= string.Empty;
public EditorContext(string CurrentEditorText)
{
m_CurrentText = CurrentEditorText;
}
public string CurrentDocumentText
{
get{return m_CurrentText;}
set{m_CurrentText = value;}
}
}
Once this class is ready, we can just perform an action on the current editor text like so:
private void ExecutePlugin(IPlugin plugin)
{
//create a context object to pass to the plugin
EditorContext context = new EditorContext(txtText.Text);
//The plugin Changes the Text property of the context
plugin.PerformAction(context);
txtText.Text= context.CurrentDocumentText;
}
}
Summery
We've seen that it's pretty simple to support plugins in your application.
· Create A shared interfaces library
· Create Custom plugins implementing the custom interfaces
· Create Context arguments to pass to the plugins
· Create a section in your config file to hold plug-in names
· Instantiate plugins using an IConfigurationSectionHandler implementer class
· Call your plugins!
· Go home and spend some quality time away from your computer