65.9K
CodeProject 正在变化。 阅读更多。
Home

通过提供插件机制为应用程序添加运行时功能

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (38投票s)

2003年5月27日

8分钟阅读

viewsIcon

166915

downloadIcon

3421

和谐地使用 Activator 和 IConfigurationSectionHandler 为您的应用程序添加插件功能

Sample screenshot

Sample screenshot

涵盖的问题
  • 使用 Activator 类
  • 通过实现 IConfigurationSectionHandler 使用自定义配置节
  • 设计简单的插件架构

您需要什么

  • VS.Net 2003 (如果您有 2002,请使用 此实用工具 将文件转换为以前的格式)

为什么我们需要为我们的应用程序构建插件框架?

人们通常出于以下原因在他们的应用程序中添加插件支持

  • 我们希望允许我们的应用程序通过添加更多功能来扩展,而无需重新编译并分发给客户
  • 我们需要在现场添加功能
  • 我们需要在现场修复一个 bug
  • 应用程序的业务规则经常更改,或者经常添加新规则

案例研究

在我们的案例研究中,我们将构建一个非常简单的文本编辑器,它只包含一个窗体。文本编辑器唯一能做的事情就是在一个位于窗体中央的文本框中显示文本。一旦这个应用程序准备好,我们将创建一个简单的插件并将其添加到应用程序中。该插件将能够读取文本框中当前的文本,解析出有效的电子邮件地址,然后返回一个只包含这些电子邮件的字符串。然后我们将把这个文本放在文本框中。

正如您所见,我们的案例研究中有一些“未知数”

  • 我们如何在应用程序中找到插件?
  • 插件如何知道文本框中的文本是什么?
  • 我们如何激活这个插件?

在构建解决方案的过程中,当我们遇到这些问题时,我们将一一解答。

第一步 – 创建一个简单的文本编辑器

好的。我不会在这里详细介绍。所有内容都在源文件下载中。只是一个显示一段文本的简单窗体。

从现在开始,我将假设您已经创建了这个简单的应用程序。

第二步 – 创建插件 SDK

现在我们有了一个应用程序,我们希望它能够与外部插件通信。我们如何实现这一点?

解决方案是让应用程序针对一个已发布的接口进行工作 – 所有自定义插件都将实现的公共成员和方法的集合。

我们将这个接口称为 IPlugin。从现在开始,任何想为我们的应用程序创建插件的开发人员都必须实现这个接口。

这个接口将位于一个共享库中,我们的应用程序和任何自定义插件都将引用它。

那么,让我们定义这个接口。对于我们简单的插件,我们需要的数据很少 – 它的名称,以及一个方法,该方法将指示它基于我们应用程序中的数据执行通用操作。

interface IPlugin
{
    string Name{get;}
    void PerformAction(IPluginContext context);
}

代码非常直接,但我将解释为什么我向 PerformAction 发送一个 IPluginContext 接口。我发送一个接口而不是仅仅一个字符串的原因是我希望允许在发送什么对象方面有更大的灵活性。目前,这个接口非常简单

public interface IPluginContext
{
    string CurrentDocumentText{get;set;}
}

现在,我只需要在一个或多个对象中实现这个接口,然后将其发送给任何插件以接收结果。将来,这将允许我更改的不仅仅是文本框,而是任何我喜欢的对象。

第三步 – 创建我们的自定义插件

现在我们只需要

  • 创建一个单独的类库对象
  • 创建一个实现 IPlugin 接口的类
  • 编译该类并将其放在主应用程序的相同文件夹

这是 EmailPlugin 类,完整版

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();
   }
}

第四步 – 让我们的应用程序了解新插件

编译了我们的插件后,我们如何让应用程序知道它?

解决方案很简单

  • 创建一个应用程序配置文件
  • 在配置文件中创建一个节,列出所有可用的插件
  • 为该配置节创建一个解析器

好的。为了处理第一步,只需将一个 XML 文件添加到主应用程序中。

提示将此文件命名为 App.Config。如果您这样做,每次构建应用程序时,VS.NET 都会自动将此文件复制到构建输出文件夹并将其重命名为 <yourApp>.Config,从而为您省去麻烦。

现在,我们希望插件开发人员能够轻松地在 Config 文件中添加一个条目来发布他们创建的每个插件。

Config 文件应如下所示


<configuration>
   <configSections>
      <section name="plugins"
               type="Royo.PluggableApp.PluginSectionHandler, PluggableApp" />
   </configSections>
   <plugins>
      <plugin type="Royo.Plugins.Custom.EmailPlugin, CustomPlugin" />
   </plugins>
</configuration>

注意 configSections 标签。我们告知应用程序配置设置,在此配置文件中有一个未识别的节,但我们有一个该节的解析器。该解析器位于 Royo.PluggableApp.PluginSectionHandler 类中,该类位于名为 PluggableApp 的程序集中

我将在接下来的内容中向您展示此类的代码。

接下来,我们有配置文件中的 Plugins 节,它为每个插件列出了类名及其所在的程序集名称。

稍后,我们将使用此信息来实例化插件。

好的。一旦 Config 文件完成,我们就基本完成了圆圈的一端。插件已准备就绪,并已发布到所有必需的通道。现在我们剩下要做的就是允许我们的应用程序读取这些信息,并根据这些信息实例化已发布的插件。

第五步 – 使用 IConfigurationSectionHandler 解析配置文件

为了解析我们应用程序配置文件中的插件,框架提供了一个非常简单的机制,该机制使我们能够将一个特定类注册为一个特定配置段的“处理程序”。我们必须为文件中不是由框架自动解析的任何段提供一个处理程序,否则将抛出 ConfigurationException

为了提供解析“plugins”节的类,我们只需要实现 System.Configuration.IConfigurationSectionHandler 接口。

接口本身非常简单

public interface IConfigurationSectionHandler
{
   public object Create(object parent, object configContext, 
                        System.Xml.XmlNode section);
}

我们所要做的就是覆盖我们自定义类中的“create”方法,并解析提供给我们的 XML 节点。在此情况下,此 xml 节点将是“Plugins” XML 节点。一旦我们有了它,我们就拥有了实例化我们应用程序插件所需的所有信息。

我们的自定义类必须提供一个默认构造函数,因为它在运行时由框架自动实例化,然后在其上调用“Create”方法。

这是 Plugins Section Handler 类的代码

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;
}

正如我们在前面提到的配置文件中看到的,我们使用 configSection 标签(位于实际的 plugins 标签之前)提供了框架处理 plugins 节所需的数据。

<configuration>
   <configSections>
       <section name="plugins" type="Royo.PluggableApp.PluginSectionHandler, 
                PluggableApp"/>

   </configSections>
   ...

注意我们如何指定类;字符串由两部分组成:类的完整名称(包括封装的命名空间),逗号,该类所在的程序集的名称。这就是框架实例化一个类所需的所有信息,毫不奇怪,这正是我们的应用程序所需的任何插件注册信息。

实例化和调用插件

让我们看看如何实际实例化一个插件实例,给定以下字符串

String ClassName = "Royo.Plugins.MyCustomPlugin, MyCustomPlugin"
IPlugin plugin = (IPlugin )Activator.CreateInstance(Type.GetType(ClassName));

让我们解释一下这里发生了什么。

由于我们的应用程序没有直接引用自定义插件的程序集,因此我们使用 System.Activator 类。Activator 是一种特殊的类,它能够根据任何数量的特定参数创建对象实例。它甚至可以创建对象的 COM 实例并返回它们。如果您曾经用 ASP 或 VB 编码过,您会记得用于根据类的 CLSID 实例化和返回对象的 CreateObject() 函数。Activator 的工作原理相同,但使用不同的参数,并返回一个 System.Object 实例,而不是一个变体……。

在此调用 Activator 时,我将我想要实例化的 Type 作为参数传递。我使用 Type.GetType() 方法返回一个与插件的 Type 匹配的 Type 实例。请注意,Type.GetType 方法接受的参数正是放在 plugins 标签内的字符串,该字符串描述了类的名称及其所在的程序集。

一旦我获得了插件的实例,我就将其转换为 IPlugin 接口,并将其放入我的插件对象中。在此行上必须放置一个 Try-Catch 块,因为我们不能确定那里描述的插件实际上是否存在,或者它是否确实支持我们所需的 IPlugin 接口。

一旦我们获得了插件的实例,我们就将其添加到我们的应用程序插件的 ArrayList 中,然后处理下一个 XML 节点。

这是我们应用程序中的代码

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;
}

调用插件

完成所有这些工作后,我们现在可以使用插件了。不过,还有一件事缺失。请记住,IPlugin.PerformAction() 需要一个 IPluginContext 类型的参数,该参数包含插件执行其工作所需的所有必要数据。我们将实现一个实现该接口的简单类,每当我们调用插件时,我们都会将其发送给 PerformAction 方法。这是该类的代码

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;}
    }
}


当这个类准备好后,我们就可以像这样对当前编辑器文本执行一个操作
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;
}

摘要

我们已经看到,在应用程序中支持插件非常简单。

  • 创建一个共享接口库
  • 创建实现自定义接口的自定义插件
  • 创建要传递给插件的上下文参数
  • 在配置文件中创建一个节来保存插件名称
  • 使用 IConfigurationSectionHandler 实现类来实例化插件
  • 调用您的插件!
  • 回家花一些高质量的时间远离电脑
© . All rights reserved.