使用.NET Framework在运行时生成并执行自定义控件(一)

翻译|其它|编辑:郝浩|2006-03-10 10:13:00.000|阅读 1757 次

概述:

# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>



本页内容
 代码生成基础知识
 使用 Reflection.Emit 生成类
 使用 System.CodeDom 生成类
 存储生成的程序集
 保护生成的程序集
 Windows 窗体示例
 使用 Reflection.Emit 生成 Windows 窗体控件
 发出程序集
 定义类型
 定义构造函数
 定义 InitializeComponent 方法
 使用 System.CodeDom 生成 Windows.Forms 控件
 定义并编译程序集
 使用 CodeDOM 定义类型
 定义构造函数
 定义InitializeComponent
 ASP.NET 示例
 数据绑定
 简化代码
 小结

Microsoft® .NET Framework 常被忽略的一个功能是在运行时生成、编译和执行自定义代码。例如,在 XML 数据的序列化过程中完成该操作;在使用正则表达式时完成该操作(其中表达式求值函数在运行时发出)。

本文描述可使用运行时代码生成功能的另一个领域 — 创建 UI 控件。这些控件只需生成一次即可在以后需要的时候进行重用,这比每次需要窗体或页面时都生成控件更有效率。

这适用于具有用户可配置字段(例如,最终用户可以选择要在屏幕上显示的数据项)的任何应用程序。通常使用 XML 定义自定义窗体。然后在运行时对其进行分析,以便在加载页面时动态构造 UI。然而,分析通常在每次显示窗体时进行或者在一个服务器方案中针对每个用户进行,这就给应用程序带来了不必要的负担。

本文,我将提供一些详尽的示例来说明如何使用运行时代码生成在运行时构造、加载和执行控件。我要描述的示例同样可以很好地应用于 .NET Framework 1.x 和 .NET Framework 2.0。在 Framework 的 2.0 版本中,针对 Reflection 命名空间添加了一些重要功能,但这些更改不会以任何方式否定或削弱本文列出的解决方案。

代码生成基础知识


许多应用程序提供可以自定义的用户界面 (UI) — 方法可能是添加额外的字段或者修改现有字段的顺序和位置。这通常利用以下两种方式之一完成:

用户通过在 Visual Studio® 中编辑用户界面进行更改。

应用程序在运行时根据某种形式的配置数据(通常存储在 XML 文件中)生成控件。

这两种解决方案都不理想。实际需要的是将这两种方法结合起来,以便提供手工操作的性能同时获得在运行时生成这些控件的应用程序的灵活性。

两种支持在运行时构造控件的方法:Reflection.Emit 和 System.CodeDom。前者(需要熟悉中间语言 (IL))使用显式指定的 IL 指令生成托管程序集。后者使用代码的对象模型生成源代码,然后将其编译为 IL 并执行。本文对这两种方法都进行了介绍。

实际上还有另一种方法(它是 CodeDOM 方法的变体),它手动构造源代码(这意味着您需要自己编写 .cs 或 .vb 文件),而不是使用对象模型进行此操作,然后在运行时编译它。虽然这是可行的,但我还是建议您使用前面提到的机制,而不要使用手工解决方案。

使用 Reflection.Emit 生成类

Reflection.Emit 命名空间允许您生成程序集(完全是临时的);它们可以在内存中生成,并且可以在无需保留到磁盘的情况下执行。然而,如果需要,也可以选择写入磁盘。(稍候我将在本文中讨论这一内容。)

要使用 Reflection.Emit 生成一个类,需要采取以下步骤:

定义动态程序集。

在该程序集中创建一个模块。

在该模块中定义一个类型(以便从适当的代码基类和该类型的方法 interface(s):Create 派生类型),获取每个方法的 ILGenerator,在适当的时候发出 IL 操作码,以及保持该类型。

可以选择保存该程序集以供将来使用。

使用 Reflection.Emit 构造类是一个复杂的方法,而且对于任何给定的源代码操作(例如,调用一个方法),通常不得不生成若干行 IL 代码。稍后我们将对此进行讨论。

使用 System.CodeDom 生成类

System.CodeDom 命名空间提供一个语言不可知的方式来定义类型。您可以构造代码的内存中模型,然后使用代码生成器发出该模型的源代码。只要有适当的代码生成器,就能够以任何语言生成代码(.NET Framework 包括可重新发布的用于 Visual Basic®、C# 和 JScript® 的代码生成器;如果安装了 Visual Studio,也可以使用针对 C++ 和 J# 的生成器)。

要使用 CodeDOM 生成一个类:

创建一个新 CodeCompileUnit。

为 CodeCompileUnit 添加命名空间。

在适当的情况下添加导入语句。

为要构造的类添加 CodeTypeDeclaration。

为每个方法添加 CodeExpressions。

获取代码编译程序并编译 CodeCompileUnit。

该方法的一个好处是,允许使用更高级的概念(较之于使用 Reflection.Emit 而言)。

定义类之后,需要在运行时构造它们的实例。最常用的方法是使用 Activator 类加载该类型,如以下代码片段所示:

public Control LoadControl ( string typeName )
{
    return LoadControl(Type.GetType(typeName));
}

public Control LoadControl ( Type controlType )
{
    return Activator.CreateInstance(controlType) as Control;
}

这里我调用 Type 类的 GetType 成员。GetType 返回在其参数中命名的类型的 .NET 类型信息。然后用 Activator 类构造该类型的一个实例。注意,如果指定类型位于一个除 mscorlib 或当前执行程序集之外的程序集中,则必须使用程序集的名称限定类型名称。当作为字符串存储时,托管类型的名称可用该类型的命名空间进行部分限定,或者使用该类型的命名空间和存储该类型的程序集名称进行完全限定。以下是一个程序集限定的类型名称的示例:

TestControls.TestControl, TestAssembly

该类型名对应于下列代码(编译为名为 TestAssembly 的程序集)中显示的类型:

using System.Web.UI;

namespace TestControls
{
  public class TestControl : Control
  {
    ...
  }
}

生成控件时,通常将类型名存储在持久的存储媒介中(例如,SQL Server 数据库)。当呈现窗体或页面时,您将使用 Activator.CreateInstance 加载该类型,然后向用户显示将该对象。

存储生成的程序集

使用 Reflection.Emit 或 System.CodeDom 创建的程序集可以是完全临时的,也可以在磁盘上生成以便将来使用。临时程序集仅在创建它们的应用程序域的预期生命周期中存在。一旦卸载应用程序域(卸载应用程序时卸载默认的应用应用程序域),这些程序集就从内存卸载并且不再存在。由于在运行时生成这些程序集会引起性能损耗,因此我建议您将生成的程序集保留到文件系统中,并先在那里查找它们。如果在运行时无法从文件系统找到程序集,则可以重新生成它。当然,如果选择该方法,需要确保控件的定义和生成的程序集保持同步。对于商业应用程序而言,我建议您为用户提供一个可通过需要生成的所有控件运行的可管理工具,并将它们存储在磁盘上或 SQL Server® 中的单个程序集中。

此外,我也建议您将动态生成的程序集数量限制为最小的实际数量 — 最好是一个。这意味着,每次在任何控件中进行更改时,都需要生成整个控件集。然而,由于这是一个不常发生的管理活动,因此它不会产生性能问题。

保护生成的程序集

另一个重要的方面是了解与生成的程序集相关的安全问题。这是非常重要的,因为生成的程序集会给应用程序带来特别的威胁 — 即,有问题的应用程序期望动态加载某些类型,而且从理论上讲,这些类型可以执行任意代码。

保护生成代码的首选方法是为所有生成的代码赋予受限的权限集。这可以在 .NET 中完成,方法是在 .NET 配置工具 mscorcfg.msc 中指定一个代码组。

自定义代码组可以使用特定的成员条件和权限集来定义。例如,您可能为所有动态生成的代码赋予一个非常受限的权限集,例如,Execute(它允许程序集执行,但严格限制代码的操作)。代码组通过成员条件应用于程序集 — 实际上,这用于将包含的内容定义到该组中。

有若干种成员条件类型。在本示例中最常用的可能是 URL 权限,它用于基于磁盘上的特定目录定义成员关系(例如,将“file://E:/Code Generation/bin/Debug/*”用作 URL)。在该工具中定义的权限集将应用于存储在指定文件夹中的任何生成的程序集。

例如,生成控件一次并在运行时加载它们,比使用窗体的 XML 表示形式并在运行时计算它可以获得更高的性能。加载 XML 表示形式通常涉及实例化并加载 XmlDocument,与实例化常用控件相比,这是一个相对较慢的操作,即使使用 Activator.CreateInstance 也是如此。

Windows 窗体示例

本节提供关于如何在运行时使用 Reflection.Emit 和 System.CodeDom 生成 Windows® 窗体控件的示例。生成的控件将提供只由一个静态文本框组成的简单用户界面,如图 1 中所示。


图 1控件生成示例应用程序


生成的控件是红线内显示的部分。虽然这是一个简单的示例,但是它提供了可以扩展以提供更加完整的实现所需的全部概念。

我们需要的是其源代码模仿如图 2 所示的代码的控件。该代码说明了几个概念,无论您发出多少代码都可以使用这些概念:

该类从一个基类派生。

该类包含一个私有字段。

该类包含一个公共默认构造函数。

该类包含一个私有方法。

该类显示如何使用属性。

该类显示如何调用方法。

该类显示如何构造新的对象实例。

这并不是一个所有功能的详尽列表,但它是一个很好的起点。

使用 Reflection.Emit 生成 Windows 窗体控件

Reflection.Emit 命名空间包含很少数量的类,因为它是一个非常低级的 API,而且大多数困难的工作都必须由您自己编写的代码执行。我将在以下几节中提供该代码并详细地进行介绍。

使用 Reflection.Emit 很困难 — 确保生成正确代码的唯一可行方式是编写一些测试代码,进行编译,然后检验生成的 IL(使用诸如 ildasm 或 Lutz Roeder 的 .NET Reflector 工具)。我使用该方法以便生成以下 IL。这并不是说不对其他代码进行反向工程就无法生成您自己的 IL,我只是想说,很多开发人员将 IL 视为他们的首选语言,而该领域的技巧比较少而且很难(如果您想成为 IL 专家,请参考 Serge Lidin 的书籍 Inside Microsoft .NET IL Assembler, Microsoft Press®, 2002)。对于该讨论,我已经将代码分为以下四个逻辑部分:发出 Assembly,定义类型,定义构造函数以及定义InitializeComponent 方法。

发出程序集

图 3 中的代码构造了一个动态程序集,将一个模块添加到该程序集,并为该程序集创建了一个唯一的名称。首先,构造一个程序集名称。本例,我将使用 GUID 确保该程序集具有一个足够独特的名称。

然后需要调用 AppDomain 类的静态 DefineDynamicAssembly 方法。有很多重载的 DefineDynamicAssembly 函数版本,但这里我选择调用最简单的重写。接下来,通过定义模块名和文件名创建一个模块生成器对象。在以下几节的代码之后,使用 AssemblyBuilder.Save 方法最终创建了该程序集。

定义类型

要使用 Reflection.Emit 创建类型,您需要调用 ModuleBuilder 类中的一个 DefineType 方法。有多种方法定义该类、它的基类,以及它显式实现的任何接口。以下代码片段显示了如何定义从 System.Windows.Forms.UserControl 派生的类:

Type baseClass = typeof ( System.Windows.Forms.UserControl ) ;
TypeBuilder typeBuilder = moduleBuilder.DefineType( 
    "MyControls.MyControl", 
    TypeAttributes.Public | TypeAttributes.Class | 
    TypeAttributes.BeforeFieldInit, baseClass ) ;

DefineType 允许您为生成的类型指定类型属性。在本例中,新类型是一个类;它具有公共的可见性,并且静态成员可以在不强制运行时初始化类的情况下对它进行调用。

然后,我构造一个将保留该标签控件的私有字段。这遵循了与上面显示的代码相似的模式,其中名称、类性和属性在单个调用中指定。如下列代码片段所示。

// Create a private field for the label
FieldBuilder labelField = typeBuilder.DefineField( 
    "_label", typeof( System.Windows.Forms.Label ) , 
    FieldAttributes.Private ) ;

定义构造函数

为了向类型添加内容,需要调用 TypeBuilder 类(前面已经创建了该对象)的一个 Define* 方法。有很多方法用于定义构造函数、事件、字段、方法以及其他结构。

图 4 中,我使用 DefineConstructor 方法创建了一个无参数的公共构造函数。我为 DefineConstructor 方法提供适当的方法属性,并在该实例中将该参数数组定义为空。从该构造函数生成的 IL 如下所示:

.method public hidebysig specialname rtspecialname instance void .ctor()
cil managed
{
   .maxstack 3
   L_0000: ldarg.0 
   L_0001: call instance void System.Windows.Forms.UserControl::.ctor()
   L_0006: ldarg.0 
   L_0007: call instance void 
           MyControls.MyControl::InitializeComponent()
   L_000c: ret 
}

请注意,在生成的 IL 以及在 C# 代码中发出该 IL 的命令之间存在近乎 1:1 的对应关系。

拥有 ConstructorBuilder 对象后,即可从它检索一个 ILGenerator。这是一个允许您将 IL 发到该程序集的对象。

严格讲,BeginScope 和 EndScope 方法在此处不是必需的,基本上等价于 C# 中的 { 和 }。此处我发出了 IL 操作码。

CLR 是基于堆栈的体系结构,因此第一个操作码 (Ldarg_0) 将一个参数(在本例中,是当前对象的 this 指针)推到计算堆栈中。第二个指令是对基类构造函数的调用。它将 this 指针弹出堆栈并调用该构造函数,因此,下一个操作码将 this 指针加载到堆栈上以准备下一次调用。

倒数第二个操作码是对 InitializeComponent 的调用,而最后一个操作码是构造函数的返回。
标签:

本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com


为你推荐

  • 推荐视频
  • 推荐活动
  • 推荐产品
  • 推荐文章
  • 慧都慧问
扫码咨询


添加微信 立即咨询

电话咨询

客服热线
023-68661681

TOP