Issues covered:
- Using the Activator Class
- Using custom configuration sections by implementing
IConfigurationSectionHandler
- 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.
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 our main
application
Here�s the EmailPlugin
Class, in full:
public class EmailPlugin:IPlugin
{
public EmailPlugin()
{
}
public void PerformAction(IPluginContext context)
{
context.CurrentDocumentText =ParseEmails(context.CurrentDocumentText);
}
public string Name
{
get
{
return "Email Parsing Plugin";
}
}
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 <yourApp>.Config ,saving you the
hassle.
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 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()
{
}
public object Create(object parent, object configContext,
System.Xml.XmlNode section)
{
PluginCollection plugins = new PluginCollection();
foreach(XmlNode node in section.ChildNodes)
{
...
}
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 using 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 somposed 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 use 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. A 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)
{
PluginCollection plugins = new PluginCollection();
foreach(XmlNode node in section.ChildNodes)
{
try
{
object plugObject = Activator.CreateInstance(
Type.GetType(node.Attributes["type"].Value));
IPlugin plugin = (IPlugin)plugObject;
plugins.Add(plugin);
}
catch(Exception e)
{
}
}
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)
{
EditorContext context = new EditorContext(txtText.Text);
plugin.PerformAction(context);
txtText.Text= context.CurrentDocumentText;
}
Summary
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