没有找到合适的产品?
联系客服协助选型:023-68661681
提供3000多款全球软件/控件产品
针对软件研发的各个阶段提供专业培训与技术咨询
根据客户需求提供定制化的软件开发服务
全球知名设计软件,显著提升设计质量
打造以经营为中心,实现生产过程透明化管理
帮助企业合理产能分配,提高资源利用率
快速打造数字化生产线,实现全流程追溯
生产过程精准追溯,满足企业合规要求
以六西格玛为理论基础,实现产品质量全数字化管理
通过大屏电子看板,实现车间透明化管理
对设备进行全生命周期管理,提高设备综合利用率
实现设备数据的实时采集与监控
利用数字化技术提升油气勘探的效率和成功率
钻井计划优化、实时监控和风险评估
提供业务洞察与决策支持实现数据驱动决策
翻译|其它|编辑:郝浩|2006-03-10 10:13:00.000|阅读 1758 次
概述:
# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>
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 生成一个类,需要采取以下步骤:
• | 定义动态程序集。 |
• | 在该程序集中创建一个模块。 |
• | 在该模块中定义一个类型(以便从适当的代码基类和该类型的方法 interface(s):Create 派生类型),获取每个方法的 ILGenerator,在适当的时候发出 IL 操作码,以及保持该类型。 |
• | 可以选择保存该程序集以供将来使用。 |
使用 Reflection.Emit 构造类是一个复杂的方法,而且对于任何给定的源代码操作(例如,调用一个方法),通常不得不生成若干行 IL 代码。稍后我们将对此进行讨论。
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 也是如此。
本节提供关于如何在运行时使用 Reflection.Emit 和 System.CodeDom 生成 Windows® 窗体控件的示例。生成的控件将提供只由一个静态文本框组成的简单用户界面,如图 1 中所示。
图 1控件生成示例应用程序
生成的控件是红线内显示的部分。虽然这是一个简单的示例,但是它提供了可以扩展以提供更加完整的实现所需的全部概念。
我们需要的是其源代码模仿如图 2 所示的代码的控件。该代码说明了几个概念,无论您发出多少代码都可以使用这些概念:
• | 该类从一个基类派生。 |
• | 该类包含一个私有字段。 |
• | 该类包含一个公共默认构造函数。 |
• | 该类包含一个私有方法。 |
• | 该类显示如何使用属性。 |
• | 该类显示如何调用方法。 |
• | 该类显示如何构造新的对象实例。 |
这并不是一个所有功能的详尽列表,但它是一个很好的起点。
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
面对“数字中国”建设和中国制造2025战略实施的机遇期,中车信息公司紧跟时代的步伐,以“集约化、专业化、标准化、精益化、一体化、平台化”为工作目标,大力推进信息服务、工业软件等核心产品及业务的发展。在慧都3D解决方案的实施下,清软英泰建成了多模型来源的综合轻量化显示平台、实现文件不失真的百倍压缩比、针对模型中的大模型文件,在展示平台上进行流畅展示,提升工作效率,优化了使用体验。
本站的模型资源均免费下载,登录后即可下载。模型仅供学习交流,勿做商业用途。
本站的模型资源均免费下载,登录后即可下载。模型仅供学习交流,勿做商业用途。
本站的模型资源均免费下载,登录后即可下载。模型仅供学习交流,勿做商业用途。
服务电话
重庆/ 023-68661681
华东/ 13452821722
华南/ 18100878085
华北/ 17347785263
客户支持
技术支持咨询服务
服务热线:400-700-1020
邮箱:sales@evget.com
关注我们
地址 : 重庆市九龙坡区火炬大道69号6幢
慧都科技 版权所有 Copyright 2003-
2025 渝ICP备12000582号-13 渝公网安备
50010702500608号