翻译|其它|编辑:郝浩|2006-03-10 10:14:00.000|阅读 1776 次
概述:
# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>
该方法比其他方法复杂得多,因为它必须进行以下操作:
• | 创建新标签控件。 |
• | 调用 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 的语言无法实现的),但它在许多应用程序中未必是必需的。
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 namespace CodeCompileUnit ccu = new CodeCompileUnit ( ) ; CodeNamespace ns = new CodeNamespace ( "MyControls" ) ; // Add some imports statements to the namespace ns.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 unit ccu.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 进行类型创建非常容易。以下代码显示如何创建派生于 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 方法(如图 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 控件。由于该代码与 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 方法。预先创建控件所获得的性能增益将使您获得良好的投资回报。
标签:
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com