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

C# 2.0 中的插件:支持泛型的扩展库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (37投票s)

2007年10月31日

CPOL

7分钟阅读

viewsIcon

97166

downloadIcon

1372

作为我上一篇文章的续篇,本文将插件概念封装到一个支持泛型的库中,并增加了对运行时源代码编译的支持。

Screenshot - PluginsInCSharp2.jpg

引言

如果您还没有阅读我(几年前发布的)第一篇文章,请先阅读。

现在您看完了,欢迎回来!我收到了关于第一篇文章的**大量**积极反馈,并且一直打算再写一篇关于这个话题的文章,但这么多年过去了。好了,它终于来了,迟到很久了!

本文的目的在于,将我们在上一篇文章中讨论过的一些功能封装到一个简单易用的库中,任何人都可以将其包含在自己的项目中。我们将更进一步,利用泛型。作为奖励,我们还将内置加载源代码文件作为插件(它们将在运行时使用 CodeDom 进行编译)的功能。因此,该库的合适名称将是 *ExtensionManager*,我正是这样称呼它的,因为它将以相同的方式加载已编译的程序集和未编译的源代码文件。

扩展

首先,让我们在此处定义什么是扩展。如果您愿意这样想,本库中有两个级别的扩展。首先,我们有一个 Extension<ClientInterface> 对象,它本身不是扩展,而是用于存储实际扩展信息的包装器。此包装器的作用只是存储我们需要的每个扩展的信息,以及实际扩展的方法和属性。

我们的扩展包装器将包含多个数据项。它将接受一个泛型参数,该参数将是插件接口(在我们上一篇文章中是 IPlugin),或者正如我们现在所称的,ClientInterface,它将指定所有可接受的扩展都需要继承的接口。

我们还希望在包装器中存储扩展的文件名以及它的类型(程序集或源文件)。即使对于程序集扩展来说这无关紧要,如果扩展是源文件,我们就需要跟踪该源文件的 Language 以便稍后编译。

最后,扩展包装器需要存储加载后的实际扩展实例。在此项目中,我们还公开了该实例的 Assembly 对象,以备将来需要。(同样,还有一个 GetType(string name) 方法,它会在扩展的程序集对象中查找类型。这是必需的,因为 Type.GetType() 方法不会在我们的扩展程序集中查找类型。)

public class Extension<ClientInterface>
{
  public Extension()
  {
  }

  public Extension(string filename, ExtensionType extensionType, 
                   ClientInterface instance)
  {
    this.extensionType = extensionType;
    this.instance = instance;
    this.filename = filename;
  }

  private ExtensionType extensionType = ExtensionType.Unknown;
  private string filename = "";
  private SourceFileLanguage language = SourceFileLanguage.Unknown;
  private ClientInterface instance = default(ClientInterface);
  private Assembly instanceAssembly = default(Assembly);

  public ExtensionType ExtensionType
  {
    get { return extensionType; }
    set { extensionType = value; }
  }

  public string Filename
  {
    get { return filename; }
    set { filename = value; }
  }

  public SourceFileLanguage Language
  {
    get { return language; }
    set { language = value; }
  }

  public ClientInterface Instance
  {
    get { return instance; }
    set { instance = value; }
  }

  public Assembly InstanceAssembly
  {
    get { return instanceAssembly; }
    set { instanceAssembly = value; }
  }


  public Type GetType(string name)
  {
    return instanceAssembly.GetType(name, false, true);
  }
}

ExtensionManager

现在,让我们关注 ExtensionManager 对象。这是该库的核心,顾名思义,它负责查找、加载和管理我们的所有扩展。

我将按照其使用逻辑顺序介绍 ExtensionManager。首先,您需要告诉管理器它应该注意哪些文件扩展名,以及如何映射它们。为此,ExtensionManager 具有两个属性:SourceFileExtensionMappingsCompiledFileExtensions

SourceFileExtensionMappings 是一个字典,负责将某些文件扩展名映射到某些语言。例如,如果我们想将自定义文件扩展名“.customcsharp”映射到 C#,我们将调用

SourceFileExtensionMappings.Add(".customcsharp", SourceFileLanguage.CSharp);

在上面的示例中,ExtensionManager 找到的所有 *.customcsharp 文件都将被编译为 C# 文件。

CompiledFileExtensions 是一个简单的字符串列表,其中包含应作为已编译程序集加载的扩展。通常,您会这样做:

CompiledFileExtensions.Add(".dll");

这将把任何 .dll 扩展文件视为已编译的程序集。

您可以选择性地调用 ExtensionManager.LoadDefaultFileExtensions() 方法,该方法会将 .cs.vb.js 加载为 SourceFileExtensionMappings,并将 .dll 加载为 CompiledExtension

下一步是告诉 ExtensionManager 在哪里查找要加载的扩展。您可以使用 LoadExtension()LoadExtensions() 方法加载单个文件或搜索目录中的文件。通常会进行如下设置,将您的扩展存储在应用程序目录的“Extensions”文件夹中:

myExtensionManager.LoadExtensions(Application.StartupPath + \\Extensions);

在这些方法中,扩展管理器将决定该文件是已编译的程序集还是源代码文件,并采取适当的操作将其加载到内存中。它将根据需要调用私有方法 loadSourceFileloadCompiledFile

加载源代码文件

由于我的第一篇插件文章已经涵盖了基于包含特定接口加载程序集的概念,因此我在这里不再赘述。但我将重点介绍将源代码文件编译并全部加载到内存中的过程。

这里的真正魔力在于 System.CodeDom.Compiler。这使我们能够相对轻松地将源代码编译成程序集!我们的 loadSourceFile 私有方法调用另一个私有方法 compileScript,该方法负责将源文件转换为 Assembly。从那里开始,其余过程与加载已编译程序集相同。唯一需要注意的是,如果存在编译错误,loadSourceFile 将会引发 AssemblyFailedLoading 事件。为此的 AssemblyFailedLoadingEventArgs 将提供有关编译错误的信息,这些信息可用于您的应用程序进行调试。

compileScript 中,我们所做的就是根据给定的语言创建一个 CodeDomProvider。默认情况下,CodeDom 支持 C#、VB 和 JavaScript。其他语言也可能得到支持,但您需要下载相应的程序集并在本项目中引用它们才能包含它们。IronPython 可能是包含在此处供您自己使用的一个不错的 CodeDom 提供程序!

除此之外,我们还设置了一些参数,告诉 CodeDom 不要生成可执行文件,并将编译到内存中。我们还指定不包含调试符号。如果您愿意,可以将此选项作为 ExtensionManager 的一个属性公开。

另一个非常重要的步骤是告诉 CodeDom 要使用哪些引用。通过此设置,您可以引用第三方程序集供 ExtensionManager 使用。这些可以在 ExtensionManagerReferencedAssemblies 属性中进行设置。

最后,我们调用编译器并返回其结果。如您所见,CodeDom **非常**简单易用!

private CompilerResults compileScript(string filename, 
        List<string> references, string language)
{            
  System.CodeDom.Compiler.CodeDomProvider cdp = 
    System.CodeDom.Compiler.CodeDomProvider.CreateProvider(language);
        
  // Configure parameters
  CompilerParameters parms = new CompilerParameters();
  parms.GenerateExecutable = false; //Don't make exe file
  parms.GenerateInMemory = true; //Don't make ANY file, do it in memory
  parms.IncludeDebugInformation = false; //Don't include debug symbols
  
  //Add references passed in 
  if (references != null)
    parms.ReferencedAssemblies.AddRange(references.ToArray());
        
  // Compile            
  CompilerResults results = cdp.CompileAssemblyFromFile(parms, filename);
        
  return results;
}

其他注意事项

我的上一篇文章一样,我有一个“Unload”扩展的方法,但这并**不能真正**卸载扩展。问题在于,我们将所有扩展加载到与主机相同的 AppDomain 中。要真正从 AppDomain 中卸载程序集,只能通过卸载 AppDomain 本身来实现。作为未来的考虑,我可能会让 ExtensionManager 将扩展真正加载到自己的 AppDomain 中。目前,这还不是一个选项。

ExtensionManager 还公开了另外几个事件。AssemblyLoadingAssemblyLoaded 提供与其名称相符的通知。

示例解决方案

我包含了一个示例解决方案,以帮助您了解如何在自己的项目中实现这一点。该解决方案包含七个项目(其中四个是扩展):

  • HostApplication – 托管我们所有扩展的启动程序。
  • Common – 仅是一个定义主机和扩展接口的程序集,所有扩展和主机都需要引用此公共程序集。
  • ExtensionManager – 我们刚才讨论过的那个库?
  • ExtensionOne – 仅更新主机状态为当前时间。
  • ExtensionTwo – 一个 .cs 文件,不是已编译的扩展,它通过操作系统版本更新主机状态。
  • ExtensionThree – 请求用户输入,然后用用户输入的内容更新主机状态。
  • ExtensionFour – 另一个 .cs 文件;这次,代码中存在错误,无法编译,但会在主机中触发错误消息。

值得注意的是,每个扩展项目都有一个生成后事件,用于将扩展(无论是 .cs 文件还是 .dll 文件)复制到 HostApplicationExtensions 文件夹中。您应该只需要编译所有内容并运行 HostApplication 即可进行演示。

结论

这个 ExtensionManager 相当简单,功能不全,但它能完成预期的任务。在我需要提供一种快速简单的方式来扩展我的应用程序的项目中,我曾无数次使用过它。

也许,它可以作为您构建的基础,或者,它可能正好满足您的需求。无论如何,我希望这篇文章对您有所帮助。祝您使用愉快!

历史

  • 2007年10月31日 - 首次提交至 CodeProject。
© . All rights reserved.