Jason Clark
本文假设您熟悉 .NET 与 C#
下载本文的代码: Plug-Ins.exe (135KB)
概述
大多数用户应用程序都受益于可由其他开发人员扩展的能力。 扩展一个用户已经很熟悉并针对它进行过培训的现有应用程序往往比从头开发来得简单和有效。因此,可扩展性会使您的应用程序更加吸引人。 您可以通过支持插件和宏等功能来使应用程序具有可扩展性。 使用 .NET Framework 可以轻松实现这一点,即使核心应用程序不是 .NET Framework 应用程序。 在本文中,作者将描述 .NET Framework 的可扩展功能(包括晚期绑定和反射)及它们的使用方式,同时还介绍插件安全注意事项。
想像一下完美的文本编辑器是什么样子的。它启动时间不超过两秒,支持针对流行的编程语言的上下文着色和自动缩进,支持多文档界面 (MDI) 以及很酷并且大受欢迎的选项卡式文档排列方式。构想这种完美的文本编辑器的问题在于完美只存在旁观者的眼中。 这些功能只是我对完美的文本编辑器的定义,其他人肯定会有不同的标准。也许完美的文本编辑器所能拥有的最重要的功能就是支持丰富的可扩展性,这样开发人员就可以使用他们需要的功能来扩展该应用程序。
可扩展的文本编辑器可能支持创建自定义工具栏、菜单、宏,甚至是自定义文档类型。 它应该允许我编写能挂接到编辑进程的插件,以便添加自动完成、拼写检查及其他诸如此类的美妙功能。 最后,完美的文本编辑器应该能让我用任何语言编写自己的插件(我个人的首选是 C#)。
诚然,我希望所用的每个应用程序都能按这种方式来扩展。 如果在某些地方编写少量代码就可以自定义自己喜欢的应用程序,那就再好不过。即使我做不到,我也知道其他人能够做到,我再通过下载来从 Internet 利用他们的扩展。这就是我开展此项活动以让所有开发人员都来编写可扩展应用程序的初衷。
理想的可扩展应用程序
许多应用程序都可以使用可插入代码来修改。 实际上,整个 Microsoft Office 应用程序 套件都可以如此广泛地进行自定义,以致人们能够使用 Office 作为平台来编写完整的自定义应用程序。 然而,即便有这么完备的可自定义能力,我还是为 Microsoft Word(一个我几乎天天使用的应用程序)编写了我的第一个插件。
原因很简单。 Microsoft Office 的所有功能并不能完全符合我的标准,包括:
• 简单性。 我想以已经很熟悉的非常简单的软件工具来操作我的可插入应用程序。
• 权限。 我想让我的插件有权访问应用程序中内置的某些对象和功能子集。 这种权限应该是自然而然的,如同我选择的编程语言的一部分。
• 编程语言。 有时我想使用特别选择的编程语言。
• 能力。 除了访问应用程序的文档对象模型 (DOM) 外,我还要一个丰富的 API。
• 安全性。 我需要能够下载其他人编写并且可以通过 Internet 下载的插件。 我希望执行有潜在威胁或错误百出的组件而不必考虑系统的安全。
所列的简短但近乎苛求。 实际上,在 Microsoft .NET Framework 发行之前,这些标准对普通应用程序而言太过严格,是无法做到的。 但现在,我可以向您展示如何使用 .NET Framework 来将所有这些可扩展性功能添加到您的托管和非托管应用程序中。
.NET Framework 可扩展性功能
可扩展性构建在晚期绑定之上,它是指在运行时而非编译时(更典型的情况)发现和执行代码的能力。 在这几年中有许多技术创新对晚期绑定做出了重大贡献,其中包括 DLL 和 COM API。 .NET Framework 将晚期绑定的简单性提高到一个全新的层次。 为加深理解,我们来看一个非常简单的代码示例。
图 1 显示了使用反射在托管对象中执行晚期绑定是如何的简单。 如果您在 LateBinder.exe 内构建 图 1 中的代码并运行它,则可以将任何程序集(比如从图 2 中的代码构建的程序集)的名称作为命令行参数传递给给它。 LateBinder.exe 会反射程序集并在该程序集中创建从 Form 派生的类的实例,并使它们成为它自己的 MDI 子类。 .NET Framework 中的反射使晚期绑定大大简化。
反射是 .NET Framework 的基本工具之一,它促进了可扩展性应用程序的开发。 它是我这里提到的可使应用程序可扩展的四种功能之一。
公共类型系统 使用 .NET Framework 一段时间之后,您可能就会开始认为公共类型系统 (CTS) 理所当然了。不过,它的确是使该平台中可扩展性变得如此简单的原因之一。 CTS 定义了所有托管语言都必须遵循的部分面向对象特征,例如对派生的规则、命名空间、对象类型、接口和基元类型。 CTS 的这些基本规定是针对公共语言运行库 (CLR) 运行的代码设置的。
反射 反射是在运行时发现信息(例如,程序集实现的类型或类型定义的方法等信息)的能力。 之所以反射成为可能,是因为所有托管代码都是通过嵌入到程序集中的数据结构(称为元数据)自描述的。
Fusion .NET Framework 使用 Fusion 来将程序集加载到托管进程 (AppDomain) 中。 Fusion 有助于实现一些高级功能,如强命名和简化 DLL 的搜索规则。
代码访问安全性 代码访问安全性 (CAS) 是 .NET Framework 的一个功能,可以简化部分受信任的代码的执行。 简而言之,您可以使用 Microsoft .NET Framework 的功能来限制晚期绑定代码可以访问的内容,这样就不用担心插件破坏用户的系统。
这就是.NET Framework 使可扩展性变成现实的四个功能。 然而,由于这些功能是如此的酷,所以一篇文章介绍一个功能不可能将可扩展性讲得非常透彻。 因此,最好的做法是从一个任务出发引入这个话题。
可扩展性入门
不管您的应用程序有什么用途,只要它是可扩展的,就必须执行三个基本任务: 发现、加载和激活扩展。发现是查找您的应用程序在运行时绑定的插件和其他代码的过程。 加载是将代码(打包为程序集)放入进程或 AppDomain 以便激活并使用由程序集定义的类型的过程。 激活是创建晚期绑定对象的实例并调用它们的方法的过程。
这三个阶段的每一个都包含着许多 .NET 技术和应用程序设计注意事项。 虽然技术上能够做到,但 .NET Framework 并没有定义一种特殊的方式来实现可扩展性,所以您可以有许多选择。
加载: 在运行时绑定到代码
从逻辑上讲,可扩展性应用程序在加载代码之前要先发现它。 但反射必须加载代码才能发现与它有关的内容,所以实际上发现过程可能在加载代码之后。 我们来看一下这是什么意思。
反射可以用来实现晚期绑定。 大多数反射类都可以在 System.Reflection 命名空间中找到。 三种最重要的类是 System.AppDomain、System.Type 和 System.Reflection.Assembly。后面我将会介绍 System.Type。 为了理解 AppDomain 和 Assembly 类,我们简单看一下托管进程。
CLR 在 Win32 进程中运行托管代码,粒度比在非托管应用程序中找到的更细。 例如,一个托管进程可以包含多个 AppDomain,您可以认为后者是一种子进程或轻量级的应用程序容器。
程序集是 DLL 和 EXE 的托管版本,它们包含可重用的对象类型(如类库类型)以及应用程序代码。 另外,应用程序的任何扩展或插件也应该存在于程序集中(与 DLL 非常类似)。 程序集也要加载到托管进程内的一个或多个 AppDomain 中。
每个托管进程至少有一个默认 AppDomain,同时包含某些共享资源,例如托管堆 (heep)、托管线程池和执行引擎本身。除了这些逻辑组件之外,托管进程还可以创建任意数量的 AppDomain。请参见 图 3,它显示了一个包含两个 AppDomain 的托管线程。在后面有关插件发现的话题中,AppDomain 显得极为重要。
图 3 托管进程
现在我们回到 AppDomain 和 Assembly 类型。 您可以使用 Assembly.Load 静态方法将程序集加载到当前的 AppDomain 中。 Assembly.Load 方法将引用返回给 Assembly 对象。这个方法在运行时将代码绑定到您的应用程序,方法是加载驻留代码的程序集。
Assembly.Load 通过名称(不带扩展名)从 AppBaseDir 目录加载程序集(AppDomain 就是从这个目录秘密加载已部署程