使用.NET Framework在运行时生成并执行自定义控件
2010-08-12 11:46 xiatao 阅读(578) 评论(0) 收藏 举报使用.NET Framework在运行时生成并执行自定义控件
本文讨论:
|
本文使用了以下技术: |
本页内容
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 labelFieldBuilder 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 的调用,而最后一个操作码是构造函数的返回。
定义 InitializeComponent 方法
该方法比其他方法复杂得多,因为它必须进行以下操作:
| • |
创建新标签控件。 |
| • |
调用 SuspendLayout。 |
| • |
设置标签控件的所有属性。 |
| • |
将标签控件添加到用户控件的 Control 集合中。 |
| • |
设置用户控件的初始大小。 |
| • |
调用 ResumeLayout。 |
这些任务在图 5 中列出。生成该控件需要很多代码,而且易于出错。但是,您可以将它分成多个单独的函数以便于阅读。
图 5 中的多数代码片段进行了类似的操作 — 它们使用 Ldarg_0 操作码将 this 指针加载到该堆栈上,然后将参数加载到该堆栈上。然后,这些参数用于调用属性 setter,或者调用一个方法调用(在某些情况下)。例如,设置控件大小的代码如下所示:
initIL.Emit ( OpCodes.Ldarg_0 ) ;initIL.Emit ( OpCodes.Ldc_I4, 328 ) ;initIL.Emit ( OpCodes.Ldc_I4, 200 ) ;initIL.Emit ( OpCodes.Newobj , typeof ( Size ).GetConstructor( new Type[] { typeof ( int ) , typeof ( int ) } ) ) ;initIL.Emit ( OpCodes.Callvirt , controlClass.GetProperty("Size", typeof(Size)).GetSetMethod ( ) ) ;
我将 this 指针加载到该堆栈,后跟整数 328 和 200。然后对 Size 构造函数进行调用,它使用这两个整数构造一个 size 对象。该 size 对象在堆栈的顶部返回。然后,获取该控件的 Size 属性,并检索该属性的 setter(我使用堆栈上的当前参数调用它)。直接的结果是我生成了以下调用:
this.Size = new Size ( 328, 200 ) ;
由于该解决方案固有的复杂性,我建议,在没必要的情况下不要广泛使用它。以下的解决方案使 IL 的生成更加灵活,而无需开发人员深入学习即可编写代码。IL 可以完成一些高深的任务(这是使用其他针对 CLR 的语言无法实现的),但它在许多应用程序中未必是必需的。
使用 System.CodeDom 生成 Windows.Forms 控件
CodeDOM 命名空间提供一个抽象的代码模型,您可以针对它进行编程。用极为简单的表达式即可在内存中构建该模型,然后利用一个可用的 .NET 代码生成器将其转换为源代码。目前,Microsoft 提供针对 C#、Visual Basic、C++、JScript、J# 和 MSIL 的代码生成器。还可以从第三方获得其他语言的生成器。
我已经将该代码分成如 Reflection.Emit 示例中所示的几个部分,以便直接比较每个步骤。请注意,代码生成的两个方法是不可互换的 — 您无法在同一个程序集中将 CodeDOM 与发出的代码混合。
定义并编译程序集
使用 CodeDOM 的第一步是引用 System.CodeDom 命名空间。然后,需要创建一个 CodeCompileUnit,它将构成该程序集的基础。这里,可以添加命名空间,将导入语句添加到这些命名空间,以及定义类型。
以下代码显示如何创建特定程序集。我已经创建了一个 CodeCompileUnit,将 MyControls 命名空间添加到其中,然后定义了代码中使用的所有类的导入语句。然后,该命名空间添加到代码编译单元:
// Create a code compile unit and a namespaceCodeCompileUnit ccu = new CodeCompileUnit ( ) ;CodeNamespace ns = new CodeNamespace ( "MyControls" ) ;// Add some imports statements to the namespacens.Imports.Add ( new CodeNamespaceImport ( "System" ) ) ;ns.Imports.Add ( new CodeNamespaceImport ( "System.Drawing" ) ) ;ns.Imports.Add ( new CodeNamespaceImport ( "System.Windows.Forms" ) ) ;// Add the namespace to the code compile unitccu.Namespaces.Add ( ns ) ;
当完成类型定义后,可以利用以下代码来使用 C# 代码提供程序,以便将该定义编译为一个程序集。这里,我已经包含了编译步骤以便完成该部分。然而,从逻辑上讲,当在程序集中创建这些类型后,这种情况通常不会发生(本文稍后将对此进行描述)。
CodeDomProvider provider = new Microsoft.CSharp.CSharpCodeProvider ( ) ;ICodeCompiler compiler = provider.CreateGenerator ( ) as ICodeCompiler ;CompilerParameters cp = new CompilerParameters ( new string[] { "System.dll", "System.Windows.Forms.dll", "System.Drawing.dll" } ) ;cp.GenerateInMemory = true;cp.OutputAssembly = "AutoGenerated";CompilerResults results = compiler.CompileAssemblyFromDom ( cp, ccu ) ;
这里针对 C# 创建了 CodeDomProvider,而且对 CreateGenerator 的调用返回了一个 ICodeCompiler 接口。然后,我定义了应该从该程序集引用的库。对该 CompilerParameters 类设置了一些参数后,我调用 CompileAssemblyFromDom 方法。根据在该代码中指定的设置,在内存中生成了一个名为 AutoGenerated 的程序集。
CompilerResults 类包含一个在成功编译该程序集时设置的程序集属性。然后,您可以从该程序集加载类型或者进行其他的任何适当的操作。
使用 CodeDOM 定义类型
使用 CodeDOM 进行类型创建非常容易。以下代码显示如何创建派生于 UserControl 的类:
CodeTypeDeclaration ctd = new CodeTypeDeclaration ( "MyControl" ) ;ctd.BaseTypes.Add ( typeof ( System.Windows.Forms.UserControl ) ) ;ns.Types.Add ( ctd ) ;
CodeTypeDeclaration 包含一个名为 BaseTypes 的集合,它包含一个从中派生类型的类。它也包含该类型应该实现的接口。请注意,即使这是一个任意类型的集合,您也无法在此使用两个或更多的类,因为 CLR 只支持单继承。如果您试图使用一个以上的基类,则会引发错误。
定义构造函数
当具有 CodeTypeDeclaration 时,您可以在该声明中添加所有其他的代码。以下代码片段使用了在定义构造函数时创建的 ctd 变量:
CodeConstructor constructor = new CodeConstructor ( ) ;constructor.Statements.Add ( new CodeMethodInvokeExpression ( new CodeThisReferenceExpression ( ) , "InitializeComponent", emptyParams ) ) ;constructor.Attributes = MemberAttributes.Public ;ctd.Members.Add ( constructor ) ;
构造函数(以及从 CodeMemberMethod 派生的其他类型)包括名为 Statements 的 CodeStatementCollection 属性。为此,需要以 CodeStatement 对象的形式添加一个或多个语句(如果您添加 CodeExpression,它将自动包装在 CodeExpressionStatement 对象中)。本例,我添加了一个 CodeMethodInvokeExpression,它调用 this.InitializeComponent,并且不传递参数。然后,我将该构造函数指定为可公共访问的,并最终将它添加到类型定义中。
定义InitializeComponent
InitializeComponent 方法(如图 6 所示)显然比该构造函数更复杂,因为需要创建许多语句。虽然有些冗长,但是该代码比 IL 等价代码更容易编写。让我们详细分析该代码的一些片段,以便于您了解它的工作原理。首先,让我们看一下 SuspendLayout 调用:
this.SuspendLayout ( ) ;
该表达式通过使用 CodeMethodInvokeExpression 生成,并传递对调用该方法的对象得的引用。后面是方法名和需要传递给该方法的所有参数。CodeMethodInvokeExpression 以如下方式调用: CodeMethodInvokeExpression ( targetObject, methodName, parameters )在该实例中,targetObject 是 this,methodName 是 SuspendLayout,而且没有参数,因此在代码中它按如下方式定义(emptyParams 是类型 CodeExpression 的一个空数组):
initializeComponent.Statements.Add( new CodeMethodInvokeExpression( new CodeThisReferenceExpression(), "SuspendLayout", emptyParams));
下一个示例显示如何将属性设置为特定值。在该实例中,发出的代码是:
_label.TabIndex = 0 ;
这里您需要使用 CodeAssignmentStatement,它有两个参数:一个是 left-hand side,也称为 lhs(需要分配的内容),另一个是 right-hand side,也称为 rhs(要分配的对象)。在该实例中,lhs 是一个 CodePropertyReferenceExpression,而 rhs 是一个 CodePrimitiveExpression:
initializeComponent.Statements.Add( new CodeAssignStatement( // This forms the left hand side and is // _label.TabIndex new CodePropertyReferenceExpression( new CodeVariableReferenceExpression ( "_label" ), "TabIndex"), // This is the right hand side new CodePrimitiveExpression ( 0 ) ) ) ;
CodePropertyReferenceExpression 本身具有两个参数:定义属性的对象和属性名。这里我使用了 CodeVariableReferenceExpression(它引用 _label 对象),并将感兴趣的属性设置为 TabIndex。
我已经使用 CodeAssignStatement 调用对其进行了处理,它将值 0 赋给该属性。CodePrimitiveExpression 可用于某些内置类型,例如,字符串和整型。
枚举值示例演示如何在代码中使用枚举值。枚举值存储为枚举类型的字段。因此,要获取给定字段的值(例如,本示例中的 AnchorStyles.Left 值),需要使用 CodeFieldReferenceExpression。
对于更复杂的枚举值(其中的值是多个字段的组合),需要使用 CodeBinaryOperatorExpression 合并它们。例如,可以用于将字段值合并在一起,在定义 AnchorStyle 属性时这是很常用的。它们通常使用逻辑 OR 进行合并,而且作为 field1 | field2 发给代码。
在构造的代码中,在几个位置都创建了对象并将它们分配给适当的属性。其中的一个示例如下所示:
_label.Location = new System.Drawing.Point(8, 8);
生成它的代码如下所示:
initializeComponent.Statements.Add( new CodeAssignStatement( new CodePropertyReferenceExpression( // Equates to this._label new CodeVariableReferenceExpression ( "_label" ), "Location"), new CodeObjectCreateExpression( // Eguates to new Point (8,8) typeof ( System.Drawing.Point ) , new CodeExpression[]{ new CodePrimitiveExpression ( 8 ) , new CodePrimitiveExpression ( 8 ) } ) ) );
我使用常用的 CodePropertyReferenceExpression 生成该语句的 left-hand side。将要创建的对象类型传递给 CodeObjectCreateExpression,然后将参数的集合传递给构造函数。在设置标签的大小以及整个控件的初始大小时,可以发现类似的代码。
ASP.NET 示例
现在,让我们看一下如何在运行时构造一个 ASP.NET 控件。由于该代码与 Windows 窗体示例中的代码非常类似,这里我省略了它。如果您想查看该代码,可以在本文的下载文件中查找。在该示例中,我创建了一个服务器控件(派生自 WebControl),它呈现由用户输入的字符串。当您运行该站点时,您可以输入一些文本,如图 7 所示,然后生成一个 IL 或 CodeDOM 示例。

图 7生成 ASP.NET 控件
单击其中一个按钮后,该代码生成一个服务器控件并将其加载到一个面板内。得到的显示如图 8 所示。尽管这不是最有用的控件,但它阐释了在运行时创建用于 ASP.NET 页控件的原理。

图 8 Web 页中的生成控件
ASP.NET 有两类控件:服务器控件和用户控件。服务器控件驻留在程序集中,通常通过创建子类 WebControl 进行创建。对于最终用户而言,它们通常具有优秀的设计时行为。
另一方面,用户控件包括带有相关代码隐藏文件的 .ascx 文件。.ascx 定义该控件的布局,而代码隐藏文件则定义行为。对本例而言,生成服务器控件远比生成用户控件容易,因此在本文中我选择了该选项。
它和 Windows 窗体示例的主要区别是如何呈现控件。使用 Windows 窗体自定义控件,每个构成控件都添加到 InitializeComponent 方法中。对于 ASP.NET 控件,您有两个用于呈现 HTML 输出的选项。第一个选项重写 Render 方法并将 HTML 显式写入输出流,从而生成该容器控件的所有内容。第二个选项用于使用添加到 CreateChildControls 方法中的控件。
在本例中,我要使用第一个方法 — Render 方法的重写。该示例虽然简单,但可以提供关于如何在运行时定义控件的基础知识。该示例可以扩展,以便添加其他属性和呈现逻辑(如果需要)。该控件在运行时使用以下代码加载:
Control ctrl = Activator.CreateInstance ( t ) as Control ;t.GetProperty("Text").SetValue ( ctrl, caption, new object[] { } ) ;this.controlPlaceholder.Controls.Add ( ctrl ) ;
类型 t 表示生成的控件,而且我已经使用 Activator.CreateInstance 对该控件进行了实例化。下一行设置 Text 属性的值,从而显示一个关于运行时邦定的示例(稍后我将对此进行介绍)。最后的代码片段将新创建的控件添加到该页的占位符控件。
有关创建自定义服务器控件的详细信息,请参阅 Developing Microsoft ASP.NET Server Controls and Components by Nikhil Kothari and Vandana Datye (Microsoft Press, 2002)。在我看来,这本书很好地介绍了有关控件创建的方方面面,如果您要定义自己的服务器控件,这可是必读的资料。
请注意,在这些示例中,我并没有将生成的控件保存到磁盘上。我这样做有两个原因。第一,也是最明显的原因是,证明您可以在无需将其保存在磁盘上的情况下生成控件;它们可以是完全临时的,因此一旦应用程序重启就会被删除。第二,也是不太明显的原因是,通过使应用程序在磁盘上生成控件,您会给应用程序带来潜在的安全缺陷。
如果应用程序要将生成的程序集保存到磁盘,则 ASP.NET 过程必须具有将 DLL 文件保存到磁盘的权限。然后,这些文件将再次加载,以便为用户显示数据。如果入侵者以某种方式获取将文件写入该目录的访问权,他可能将代码注入到您的应用程序中,从而引起严重的后果。
如果您的应用程序修改为允许生成控件,我建议您使其成为管理工具集中的一部分,并且不运行在 Web 站点中,例如,它应该成为 Windows 窗体应用程序。这将从您的数据库读取任何控件定义,生成控件,并将它们存储到磁盘或 SQL Server 数据库中。然后,ASP.NET 站点会在运行时简单地加载这些生成的控件。
数据绑定
当在页面上使用这些生成的控件时,您需要以某种方式将数据从后端数据库对象绑定到这些控件。您可以使用生成的代码(推荐方法),或者您可以将反射用作将数据绑定到 UI 的方法。本节,我将说明这两种方法。在这些示例中,我假设基础业务对象上的属性和用户界面上的控件之间存在 1:1 的对应关系。
由于本文的主要目的是提供一些策略以生成可配置的高性能解决方案,因此很明显,需要发出代码以执行数据绑定。您可以在生成控件的同时,很轻易地生成数据绑定代码(再次假设 UI 控件和基础业务对象之间直接存在 1:1 的对应关系)。应该生成的代码基本上循环通过每个属性,并将控件的文本值设置为该属性的值,如以下代码片段所示:
control.Text = businessObject.Text;
针对 ASP.NET 控件发出该代码的适当方式是作为标准 DataBind 方法(在 Control 类上定义)的一个重写。
反射可用于从您的业务逻辑组件检索数据并在控件中显示该数据。当使用 Visual Studio 时,通过将 Bindable 属性添加到属性定义,控件将属性定义为可用于数据绑定。
[Bindable(true)]public string Text { get; set; }
因此,可以使用如图 9 所示的代码检索控件的所有可绑定属性。第一行需要一个在类型 t 上定义的所有属性的集合,这些属性是公共实例属性。BindingFlags.DeclaredOnly 确保只使用直接类型的属性 — 否则,将返回该类型的继承层次结构中的所有属性。
然后,该代码循环通过这些属性,从而查找包括 Bindable 属性的任何属性。对于其中的每一个属性,它特别检查该属性是否已定义为Bindable(true),因为开发人员可能已经使用了 Bindable(false)。
使用该可绑定属性集合,可以编写如图 10 所示的代码,以便基于业务对象中的值设置控件的值。
该功能循环通过该控件的所有可绑定属性,并查看业务对象中是否存在带有相同名称和返回类型的属性。如果存在,该控件上的属性设置为业务对象上的属性值。类似的代码可用于读取用户输入到 UI 的数据,然后将该数据发送回业务对象。
简化代码
目前该示例发出的代码非常详细。为简化起见,您可以构造基类(对于 ASP.NET 而言,派生自 WebControl,对于 Windows 窗体而言,派生自控件),并包括该控件上的函数,以进行某些方面的控件生成简化工作。例如,您可以使用一组函数定义构造控件的方法,如以下代码所示:
private Label CreateLabelControl ( string ID, string caption ){ Label newLabel = new Label ( ) ; newLabel.ID = ID; newLabel.Text = caption ; this.Controls.Add ( newLabel ) ;}
您可以创建基类函数以创建各类控件。然后,发出的代码只需生成控件的实例变量,并调用这些基类函数以进行控件创建。
小结
本文阐释了如何使用两种风格的运行时代码生成(Reflection.Emit 和 System.CodeDom)生成用于 Windows 窗体和 ASP.NET 应用程序的控件。我提供了示例,并描述了用于这两种不同方法的一些实用工具。现在,我强烈建议您在自己的应用程序中使用这些方法。
如果您经常使用 XML 定义生成任何界面控件,我希望说明这些方法相关性能的一节会促使您至少考虑更改为 CodeDOM 或 Reflection.Emit 方法。预先创建控件所获得的性能增益将使您获得良好的投资回报。
Morgan Skinner 供职于 Microsoft (UK),他是一位投身于 C#、Windows 窗体和 ASP.NET 的应用程序开发顾问的工作。自从 .NET 问世以来,他就一直从事 .NET 方面的研究。请查看 Premier Support for Developers (PSfD) 小组网络日记。
浙公网安备 33010602011771号