Roy Osherove
December 2003
Summary: Shows how to add plug-in support to your .NET applications, and provides a framework you can use to add this functionality. (9 printed pages)
Applies to
Microsoft® ASP.NET
Microsoft® Visual Studio .NET 2003
Download the source code for this article.
Why Do You Need a Plug-In Framework for Your Application?
Step 1. Create a Simple Text Editor
Step 2. Create the Plug-In SDK
Step 3. Creating Your Custom Plug-In
Step 4. Letting Your Application Know About the New Plug-In
Step 5. Parse the Config File Using IConfigurationSectionHandler
Instantiating and Invoking the Plug-Ins
Invoking the Plug-ins
Summary
People usually add plug-in support in their applications for the following reasons:
In this article, youll build a very simple text editor, composed of only one form. All it will be able to do is to display text in a single textbox in the middle of the form. Once this application is ready, you'll create a simple plug-in and add it to the application. That plug-in will be able to read the text currently in the textbox, parse it for valid e-mail addresses, and return a string containing only those e-mails. You will then put this text inside the text box.
As you can see, there are a number of "unknowns" in this case study:
The answers to all of these questions will become clear as we build the solution.
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.
Now that you have an application, you will want it to be able to talk with external plug-ins. How do you make this happen?
The solution is for the application to work against a published interface, a set of public members and methods that will be implemented by all custom plug-ins. I'll call this interface IPlugin. From now on, any developer that would like to create a plug-in for your application will have to implement this interface. This interface will be located at a shared library, which both your application and any custom plug-ins will reference.
To define this interface, you need just a little data from your simple plug-in—its name, and a method that would instruct it to perform a generic action based upon the data in your application.
public interface IPlugin { string Name{get;} void PerformAction(IPluginContext context); }
The code is straightforward, but why send an IPluginContext interface to the PerformAction? The reason you send an interface rather than just a string is to allow more flexibility as to what object you will be able to send. Currently, this interface is very simple:
public interface IPluginContext { string CurrentDocumentText{get;set;} }
Now, all you 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 you to change the string of not just a textbox, but any object you like.
All you have to do now is:
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(); } }
Once you have compiled your plug-in, how do you let your application know about it?
The solution is simple:
To complete step one, just add an XML file to the main application.
TipName this file App.Config. This way, every time you build your application, Microsoft® Visual Studio .NET will automatically copy this file into the build output folder and rename it to <yourApp>.Config ,saving you the hassle.
Now, the plug-in developer should be able to easily add an entry in the config file to publish each plug-in he creates. 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. This tells the application configuration settings that you have a plug-ins section in this config file, and that you have a parser for this section. This parser resides in the class Royo.PluggableApp.PluginSectionHandler, which is in an assembly named PluggableApp. I'll show you the code for this class below.
Next, you have the plug-ins section of the config file, which lists, for every plug-in, the class name and the assembly name in which it resides. You will use this information when you instantiate the plug-in, later on.
Once the config file is done, you have finished one end of the circle. The plug-in is ready to rock, and has published itself to all the necessary channels. All you have to do now is to enable your application to read in this information, and instantiate the published plug-ins according to this information.
In order to parse out the plug-ins found within the config file of the application, the framework provides a very simple mechanism enabling you to register a specific class as a handler for a specific portion in your config file. You must have a handler for any portion in the file that is not automatically parsed by the framework; otherwise you will get a ConfigurationException thrown.
In order to provide the class that parses the plug-ins section, all you 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 you have to do is override the Create method in your custom class, and parse the XML node that is provided to you. This XML node, in this case, will be the "Plugins" XML node. Once you have that, you have all the information needed to instantiate the plug-ins for your application.
Your custom class must provide a default constructor, since it is instantiated automatically by the framework at run time, and then the Create method is called on it. Here's the code for the PluginSectionHandler 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, you provide the data the framework needs in order to handle the plug-ins section using the configSection tag prior to the actual plug-ins tags.
<configuration> <configSections> <section name="plugins" type="Royo.PluggableApp.PluginSectionHandler, PluggableApp" /> </configSections> . . .
Notice how to 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 to instantiate a class, and unsurprisingly, this is exactly the information required for any plug-ins to register for your application.
Okay, so how would you 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));
What's happening here is this: Since your application does not make a direct reference to the assembly of the custom plug-in, you use the System.Activator class. Activator is a special class able to create instances of an object given any number of specific parameters. It can even create instances of objects and return them. If you have ever coded in ASP or Microsoft® Visual Basic®, you might remember the CreateObject() function that 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, you pass in as a parameter the Type that you want to instantiate. Use the Type.GetType() method to return an instance of a Type that matches the Type of the plug-in. Notice that the Type.GetType() method accepts as a parameter exactly the string that was put inside the plug-ins tag, which describes the name of the class and the assembly it resides in.
Once you have an instance of the plug-in, cast it to an IPlugin interface, and put it inside your plug-in object. A Try-Catch block must be put on this line, since you cannot be sure that the plug-in described there actually exists, or does in fact support the IPlugin interface you need.
Once you have the instance of the plug-in, add it to the ArrayList of your application plug-ins, and move on to the next XML node.
Here's the code from the 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; }
After all this is done, you can now use the plug-ins. One more thing is missing, though. Remember that IPlugin.PerformAction() requires an argument of type IPluginContext, which holds all the necessary data for the plug-in to do its work. You'll implement a simple class that implements this interface, which you send to the PerformAction() method whenever you 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, you 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; } }
You can see that it's pretty simple to support plug-ins in your applications. You just:
Roy Osherove has spent the past 5+ years developing data-driven applications for various companies in Israel. He's acquired several MCP titles, written a number of articles on various .NET topics (most of which can be found on his weblog), and loves discovering new things everyday. Roy is also the author of the Feedable service and of the free regular expression tool, The Regulator.