下载本文的代码:CodeDOM.exe (169KB)
    摘要
    在 .NET Framework 中,CodeDOM 对象模型可以用多种语言的代码表示。本文研究使用 Framework 的 System.CodeDom 和 System.CodeDom.Compiler 命名空间编写的源代码模板是如何允许开发人员来创建可在项目间共享的可重用的样板源代码的。由模板设计的组件可提高工作效率,缩短开发时间。
    这里,通过创建 CodeDOM 对象图,可以模拟 C++ 样式的类和模板并生成多种语言的代码。另外,还说明了对象图的编译和输出代码的格式化。
    Microsoft®.NET Framework 中用 CodeDOM 以一种语言中立的方式来表示源代码文档。通过操作和使用 CodeDOM 对象图可以生成源代码,甚至生成编译后的程序集,并且可以多次使用。System.CodeDom 命名空间包括 70 多种表示典型的面向对象的高级语言(例如 C#、Visual Basic® .NET、Eiffel 或 Java 等}的特性实体的定义。System.CodeDom.Compiler 命名空间定义了处理 CodeDOM 对象图抽象类的框架,通过实现 CodeDomProvider 基类,希望支持 CodeDOM 的任何语言都可以做到这一点。
    在 .NET Framework 和 Visual Studio® .NET 中,CodeDOM 起着重要的幕后作用。ASP.NET 还用它来生成可执行代码,实际上为了在 ASP.NET 页面中使用它,用某种语言实现 CodeDomProvider 是必需的。CodeDOM 还用于 Web 服务描述语言代理生成、以及 Visual Studio .NET 的设计器和代码向导。许多语言都可以实现 CodeDomProvider 类,因而可用来处理 CodeDOM 对象图。可以预想,通过在 Microsoft.CSharp 和 Microsoft.VisualBasic 命名空间中分别定义的 CSharpCodeProvider 和 VBCodeProvider 类,C# 与 Visual Basic .NET 可以实现该类。另外,Microsoft.JScript 命名空间和 Visual J# 中还有 JscriptCodeProvider。.NET 公开了 Microsoft.VJSharp® 命名空间中的 VJSharpCodeProvider。除此之外,许多 Microsoft .NET 语言合作伙伴都已经或者正在实现 CodeDomProvider 类,作为其语言与 .NET 集成的一部分。其中包括 Interactive Software Engineering 的 Eiffel、由 ActiveState 开发的 ActivePERL 以及 Fujitsu 开发的 COBOL 语言。
    简单浏览一下 CodeDOM 之后,对象模型的一些实际应用就变得显而易见。我发现从中可以受益匪浅、同时也是本文关注的一个主要的焦点 — 就是使用 CodeDOM 创建常见的、可重用的源代码模板。这并不是个新概念。许多书、文章、语言特性和代码生成向导都讨论了这个主题。不管怎样,我觉得还是想赞美一下用 CodeDOM 来实现这些目标的优点和益处。花费一定时间了解一下相对简单的(尽管大且麻烦)对象模型,就有可能用 CodeDOM 来创建模板。另外,由于大量语言已经实现了 CodeDomProviders,因此可以用创建的模板来生成这些语言的代码,将您的模板的实用性提供给不使用您的语言的其他编程人员。
    在本文中,我将讨论 CodeDOM 对象图的创建过程,这是一个基于 Windows® 的简单应用程序使用的模板。我的前两个 C# 示例应用程序合并了这些代码。我将讨论如何使用对象图生成源代码和编译过的代码,甚至如何在内存中创建并执行多种语言的应用程序。讲完这些基本内容后,我的第二个示例将探讨使用 CodeDOM 来模仿 C++ 样式的类模板。
    创建对象图
    首先我们看看使用 CodeDOM 创建对象图模板的过程,以说明它的一些核心类以及它们是如何表示代码的。使用 CodeDOM 创建第一个对象图类似于用一种新语言编写代码。它的结构化方式表示了许多面向对象的概念。同样,为了说明它的用法,我将首先了解编程示例的要点:Hello World。下载代码中的 HelloGen 项目(参见本文顶部的链接)是该测试的基础,TemplateConstructor 类负责创建对象图。
    创建模板时,将创建的代码的示例用作引用通常很有帮助。图 1 是完整的 C# 源代码,可以作为构造该应用程序对象图的引用。应用程序本身并没什么特别的。它由两个类组成:HelloApplication 类,实例化并显示窗体;HelloWorldForm 类,定义将显示给用户的 Windows 窗体。随后您将看到,在运行时将更改该模板,以在显示该窗体时替代 TextBox txtSourceCode 的 Text 属性。
    Hello World 的全部对象图在 TemplateConstructor 类中创建,它首先调用 GenerateCCU:

    1. public CodeCompileUnit GenerateCCU() {
    2. CodeCompileUnit hwCompileUnit = new CodeCompileUnit();
    3. CodeNamespace hwNamespace = BuildNamespace();
    4. CodeTypeDeclaration hwFormClass = BuildFormClass();
    5. hwClassRef = hwFormClass;
    6. CodeTypeDeclaration hwAppClass = BuildAppClass();
    7. hwCompileUnit.Namespaces.Add(hwNamespace);
    8. hwNamespace.Types.Add(hwFormClass);
    9. hwNamespace.Types.Add(hwAppClass);
    10. return hwCompileUnit;
    11. }

    在 System.CodeDom 命名空间中公开的 CodeCompileUnit 类位于 Hello World 对象图的根部。该类表示一个项目或程序集,hwCompileUnit 表示 HelloWorld 程序集。CodeCompileUnit 是由代码提供程序和代码生成器使用的对象,包含在 CodeCompileUnit 中的所有代码都编译到单独的程序集中。
    CodeCompileUnit 公开的三个公共属性特别重要。Namespaces 属性保存有 CodeNamespace 对象的集合,可以预想,它表示了程序集内的命名空间。Referenced Assemblies 属性是一个字符串集合,表示 CodeCompileUnit 引用的程序集的名称。AssemblyCustomAttributes 属性就是一个 CodeAttributeDeclarationCollection,表示应用到程序集的属性。
    填充 CodeCompileUnit 就意味着添加 CodeNamespace 对象,这些对象将组成 Namespaces 集合 hwCompileUnit 的程序集。就像您可能已经通过查看前面 GenerateCCU 函数的代码已经发现的那样,CodeTypeDeclaration 类表示命名空间内的类。hwFormClass 和 hwAppClass 对象分别表示图 1 中的 HelloWorld 类和 AppClass 类。完成 Hello World 应用程序的基本结构只需要定义这些 CodeTypeDeclaration 对象并将它们添加到 CodeNamespace(即 hwNamespace)中即可。为了尽可能地讲清楚这一点,我已经将创建每个对象的代码移到了各自的函数中。
    CodeNamespace 对象非常简单;创建并定义该对象只需要以下几行代码:

    1. private CodeNamespace BuildNamespace() {
    2. CodeNamespace hwNamespace = new CodeNamespace();
    3. hwNamespace.Name = "HelloWorld";
    4. hwNamespace.Imports.Add(new CodeNamespaceImport("System"));
    5. hwNamespace.Imports.Add(new CodeNamespaceImport("System.Drawing"));
    6. hwNamespace.Imports.Add(new
    7. CodeNamespaceImport("System.Windows.Forms"));
    8. return hwNamespace;
    9. }

    BuildNamespace 函数创建并返回 HelloWorld 命名空间的一个表示。Imports 集合是一组 CodeNamespaceImport 对象,它表示 C# 中的 using 语句以及 Visual Basic .NET 中的 Imports 语句。CodeNamespaceImport 类的 Namespace 属性是唯一要注意的属性,在本示例中该属性在构造函数内部进行设置。
    我前面提到过,CodeTypeDeclaration 类是用来表示 CodeDOM 中的类的;但是,它还可用来表示枚举、接口和结构。通过设置适当的公共属性 — IsClass、IsEnum、IsInterface 或 IsStruct 为 True,可以通知您要表示的类型。BuildFormClass 函数负责创建 HelloWorld 类的 CodeTypeDeclaration,如图 2 所示。
    图 1 中的 C# 源代码可以看出,HelloWorldForm 类表示该应用程序的可见元素。为了在对象图中表示它,我创建了新的 CodeTypeDeclaration 来表示该类,将其命名为 HelloWorldForm,并将其 IsClass 属性设置为 True。因为 HelloWorld 类是派生于 System.Windows.Form 的一个窗体,我将 CodeTypeReference 对象添加到 BaseTypes 集合中,该集合表示 Form 基类。
    BuildFormClass 还说明了如何在 CodeDOM 中表示对象的类型信息。CodeTypeReference 类表示 CodeDOM 中的数量或数组类型,并可作为参数传递给需要定义类型的大多数 CodeDOM 构造函数和方法。其构造函数有四个重载;前两个用于数量数据类型,它们接受 String 或 System.Type 参数。当然,最安全的方法是使用 System.Type 重载;但这并不总是方便甚至可能的,这取决于正在建模的代码类型。无论怎样,文档建议(也是较好的编码做法),对于想要表示的数据类型要使用完全限定名。第三和第四个重载让您指定数组类型并接受 String 或 CodeTypeReference 作为参数。两者的第二个参数都是个整数,该整数表示数组秩。因此,接受 CodeTypeReference 作为参数的大多数 CodeDOM 类都有接受 String 或 System.Type 的重载。
    为了完整说明 HelloWorldForm 类,我设置了 Attributes 属性。Attributes 是个位字段,表示在 MemberAttributes 枚举中指定类的访问修饰符,因而通过位 OR 运算符可以应用多个修饰符。下一步,需要通过 FormClass 的 Members 集合将 TextBox 和类构造函数的声明添加到 HelloWorldForm 类中。该集合包含派生于 CodeTypeMember 的对象,并表示在 Type 内声明的实体,其中包括构造函数、成员变量和事件,这只是其中的一小部分。当构造函数重载设置成员的 Type 和 Name 属性时,还有一个可设置其修饰符的 Attributes 属性以及 InitExpression 成员(如果需要,可以设置 InitExpression 的初始值)。该类的构造函数在 BuildFormConstructor 函数中生成;但是,检查构造函数前,我先来检查 BuildAppClass 函数并表示 CodeDOM 中的 HelloApplication 类。
    图 3显示了 BuildAppClass 函数的源代码。该类中有三个新概念:表示程序集中的初始化方法,设置变量值,调用某个对象的方法。CodeEntryPointMethod 对象负责表示程序集中开始执行的方法。它继承了 CodeTypeMember 类的大部分属性,并额外添加 ReturnType 属性来表示该函数的返回类型。
    CodeStatementCollections 使您能够组合多组的单个代码语句,从而形成更高级的代码结构体,如方法、属性和 Try…catch 语句块。在图 3 中,CodeEntryPointMethod.Statements 集合表示代码语句,这些语句共同形成 Main 函数的主体。其他两个这样的示例是 CodeTryCatchFinallyStatement.TryStatements 和 CodeConditionStatement.TrueStatements,分别用来表示 try 块中的代码块和 if 语句的 True 块。创建 Main 方法只需将派生于 CodeStatement 类的对象添加到 Statements 集合中。
    添加 HelloWorldForm 类的声明后,必须用该类的一个新实例进行赋值。这可使用 CodeAssignStatement 类来完成,该类接受派生于 CodeExpression 类的两个参数。可以猜到,这是 CodeDOM 中表示表达式的抽象基类。这两个参数分别表示赋值语句的左边和右边。对于这条特定的赋值语句,我用 CodeVariableReferenceExpression 类引用了表示 HelloWorldForm 类的变量,并将变量名作为参数传递给构造函数。创建表达式由 CodeObjectCreateExpression 类表示,它接受要创建的对象类型以及在创建中要使用的参数列表。该参数列表是 CodeExpression 对象数组,而且不能为空。
    如该示例,可以发送零长度的数组来表示对无参数构造函数的调用。最后,CodeMethodInvokeExpression 类表示一种方法调用。注意,如果我想调用类的静态方法,则我就要用 CodeTypeReferenceExpression 对象取代 CodeVariableReferenceExpression。
    到目前为止,您应该可以很好地掌握了在 CodeDOM 中表示代码的方法。BuildFormConstructor 函数中只有几个新的 System.CodeDom 类,一部分在图 4 中有所表示,这也是要研究的模板的最后一部分内容。该函数的主要任务就是创建构造函数的表示形式,该构造函数处理为正确显示窗体所需要的所有属性的赋值。
    此外,派生于 CodeStatement 的类被添加到 CodeConstructor 类的 Statements 属性,与前面讲到的添加到 CodeEntryPointMethod 中一样。注意表示基元(如 CodeDOM 中的数字和字符串)的 CodePrimitiveExpression 类的用法。表示图 4 中 TextBox 指定的源代码的 Anchor 属性赋值非常麻烦。在 C# 中,赋值非常简单,但是,CodeDOM 的 CodeBinaryOperatorExpression 只接受三个参数:由 CodeExpression 类表示的表达式的左边和右边以及代表运算类型的 CodeBinaryOperatorType 枚举变量。因此,它组合了三个这样的对象在 CodeDOM 中表示。不可否认,虽然麻烦,但并不难。
    使用对象图
    创建 Hello World 应用程序模板的对象图是引入对象模型中某些重要类的必要前提,并可以说明它们是如何结合在一起来表示工作代码的。现在我们来看看如何使用该模板来生成代码。System.CodeDom.Compiler 命名空间是研究的目标。它定义了三个接口和许多抽象基类,可供各语言供应商以引用的形式使用,用来操作 CodeDOM 对象图以生成源代码和编译过的程序集。它还用来分析现有源代码,并将这些代码加入 CodeDOM 对象图。HelloGen 示例应用程序为您提供了一个这样的框架:调查和某种语言相关的这些类的一些属性,并对它们进行比较。
    图 5 显示了该应用程序的主窗体,它由两部分组成。顶部的选项卡控件允许您设置使用的代码编译器和代码生成器上的多种选项,并在编译 HelloWorld 应用程序时选择目标语言。底部的选项卡控件包括来自各种语言的输出源,通过应用程序的源代码这些语言变得可用。默认情况下,可下载的源代码设置成可以生成 C#、Visual Basic .NET 和 JScript? 语言的代码和程序集。如果想研究以其他语言生成代码来实现 CodeProviders,则源代码中的注释解释了如何将这些其他语言生成的代码添加到应用程序中。在撰写本篇文章的时候,我研究了用多种其他语言创建模板,这些语言包括 ISE 的 Eiffel、Visual J# .NET、Mondrian 以及 Fujitsu COBOL。
    生成源代码
    System.CodeDom.Compiler 命名空间的 ICodeGenerator 接口定义了用来生成来自 CodeDOM 对象图的代码的方法,特定于语言的 CodeDomProvider 实现公开了 CreateCodeGenerator 函数以返回该接口。单击 HelloGen 主窗体上的 Generate Preview 按钮,通过调用 GeneratePreview 方法启动预览过程。该方法首先获得一个 CodeGeneratorOptions 对象,该对象定义了代码生成器输出的可自定义参数,接下来我将讨论这一点。接下来,执行图 6 中的循环,生成每种语言的对象图的代码。
    第一步,通过调用 AppConstructor 类的 GenerateCCU 方法创建 HelloWorld 应用程序的编译单元。尽管最好是在循环前完成这些,但 CodeDOM 处理类型信息的方法显得有些笨拙。问题是引用下列语句中 BuildAppClass 函数的 HelloWorldForm 类型(参见图 3):

    1. MainMethod.Statements.Add(new CodeVariableDeclarationStatement(
    2. new CodeTypeReference(TypeString), "hwApp"));

    这里,TypeString 是一个表示 HelloWorldForm 类型的字符串,但 CodeTypeReference 对象并不识别 HelloWorldForm 类型,所以它只是发出 TypeString 的字符串值。对于 C# 和 Visual Basic .NET,它必须是“HelloWorldForm”,但 JScript .NET 要求它是“HelloWorld.HelloWorldForm”。为了解决这个问题,我添加了 UseFullNamespace 标志,并且循环的每次迭代都创建了新的 CodeCompileUnit。
    用 CodeDOM 创建模板的过程中,留意并正确处理这样的问题尤为重要。这种情况下,在生成 JScript .NET 代码并且不考虑其余部分的时候,我只是简单地将 AppCreator 对象的 UseFullNamespace 设置为 true。在很多情况下,都可使用 ICodeGenerator 接口的 Supports 方法来保证要想使用的生成器的适当语言功能。该方法接受 GeneratorSupport 枚举值并返回布尔值,表明该语言是否支持此特定的结构。枚举值包括了很多有用的语言功能和结构,但它没有包括全部。图 7 列出了 GeneratorSupport 枚举值,这些值都是不言而喻的。
    一旦引用了 CodeCompileUnit,就可得到期望语言的代码生成器。GetLanguageGenerator 函数实例化了派生于 CodeDomProvider 的特定于语言的代码提供程序类的一个新实例,并调用它的 CreateGenerator 方法返回该指定语言的 ICodeGenerator 接口。下面的示例说明了 C# 语言的这一点:

    1. case Language.CSharp:
    2. return new CSharpCodeProvider().CreateGenerator();

    为了创建特定于语言的源代码,我调用了 ICodeGenerator 接口的 GenerateCodeFromCompileUnit 方法(参见图 6),该方法由 CreateGenerator 函数返回。该方法接受 CodeCompileUnit(表示我的对象图)、StringWriter(将由源代码填充) 和 GeneratorOptions(可用来更改创建的代码的样式)作为参数。
    适当选项卡上显示的输出用于进行快速比较。这里,来自任何预览窗口的代码可以被剪切并粘贴到文本文件中进行编译;但在本文后面,我将研究使用 System.CodeDom.Compiler 命名空间中的对象,在 HelloGen 示例应用程序的功能程序中实现这一切的技巧。通过运行该示例,可以对 C#、Visual Basic .NET 和 JScript .NET 的输出结果进行比较,这些结果都由同一模板编译得到。另外,图 8 显示了由 ISE 的测试版 EiffelCodeProvider 生成的 HelloWorldForm 类的源代码。该示例中的代码违反了其他趋于相似的语言的语法,这强调了 CodeDOM 的语言中性和灵活性。
    格式化输出
    前面我提到过 CodeGeneratorOptions 类允许更改已生成的源代码的属性,Generator Options 选项卡提供的控件允许更改这些属性。调用 GetGeneratorOptions 时,就创建了一个新对象并设置了它的属性。有四个这样的属性:BlankLinesBetweenMembers、BracingStyle、ElseOnClosing 和 IndentString。第一个属性是 Boolean 型的,表示生成器是否在代码元素(如成员变量和类)之间插入空白行。BracingStyle 可以设置为“Block”或“c”。BlockStyle 将左大括号添加到与它们封装的代码结构所在的同一行上,否则,它们将被放到紧跟在声明之后的一行上。同样,ElseOnClosing 定义 else、catch 和 finally 语句的位置。最后,IndentString 属性用来设置每个缩进等级的字符顺序,通常最好保留为空格。
    编译对象图
    尽管只要创建模板一般就可以直接生成源代码,但是如果我不涉及 ICodeCompiler 接口,则有关 System.CodeDom.Compiler 命名空间的讨论就不完全。该接口定义将 CodeDOM 对象图编译成中间语言的方法。HelloGen 应用程序的 Create Application 按钮触发了将对象图编译成某种语言的事件,这种语言是从 Application Output 选项卡的 Target Language 下拉列表中选择的,该选项卡还包括指定代码和可执行文件目标位置的文本框。Compiler Parameters 选项卡允许设置代码编译器的可自定义属性,我在后面将讲到这点。
    该示例应用程序的 GenerateProgram 方法完成准备和编译代码的大量工作。此外,我首先生成 HelloWorld 应用程序的 CodeCompileUnit。该示例中,同时需要一个代码生成器和一个代码编译器,因此下一步我要检索它们的一个引用。GetLanguageCompiler 函数与前面讲的 GetLanguageGenerator 函数几乎完全相同,不同之处是它是通过调用 CreateCompiler 而不是 CreateGenerator 返回 ICodeCompiler 接口。下一步是使用应用程序的源代码更新 hwCompileUnit。为了预览应用程序,默认情况下,AppConstructor.GenerateCCU 创建将 TextBox txtSourceText 的 Text 属性设置为“[Little Recursive Box]”的对象图。为强调模板的跨语言功能,我希望用正在生成的 Hello World 应用程序的特定于语言的源代码来替换该文本。为此,首先用 hwGenerator 生成目前选择语言的源代码,再用 CreateExtendedCompileUnit 方法替换 CodePrimitiveExpression(参见图 9)。使用此新的特定于语言信息重新生成编译单元后,将其源代码写到文本文件中,再准备编译该代码。
    编译器选项
    与 ICodeGenerator.CreateCompileUnit 接受 CodeGeneratorOptions 对象以更改代码生成一样,CompileAssemblyFromDom 方法接受定义编译这些代码的参数的 CompilerParameters 对象。这些选项的一部分可以在应用程序的 Compiler Parameters 选项卡上设置,还可在创建时通过 GetCompilerParameters 函数在对象上设置。CompilerOptions 接受一串参数并发送到编译器,这类似于命令行的执行。GenerateExecutable 是 Boolean 属性,告知编译器是否应该编译可执行文件或 DLL。虽然通过 UI 不可见,但有一个 ReferencedAssemblies 属性,它是一个表示 CodeCompileUnit 中引用的程序集名称的字符串集合。因为 HelloWorld 是 Windows 窗体应用程序,因此我添加了对 System.Drawing.dll 和 System.Windows.Forms.dll 的引用。虽然在默认情况下 System.dll 被引用,我还是将它添加到源代码中以求完整。编译输出可使用 IncludeDebugInformation 属性设置为调试或发布,可设置 TreatWarningsAsErrors 标志告知编译器在代码中应以何种态度处理警告。
    CodeCompileUnit 是通过以 CompilerParameters 对象和上述的 CodeCompileUnit 为参数对 ICodeCompiler.CompileAssemblyFromDom 的调用进行编译的:

    1. hwResults = hwCompiler.CompileAssemblyFromDom(hwCompilerParameters,
    2. hwCompileUnit);

    hwResults 对象是 CompilerResults 类型,在 System.CodeDom.Compiler 命名空间中定义。该类包含很多用于检索(特别是通过 Errors 集合)有关编译过程信息的有用属性,这些属性将保留造成编译过程失败的任何问题的信息。如果选择将应用程序编译成一个文件,PathToAssembly 属性将反射新创建文件的位置,而如果适用,CompiledAssembly 属性将获得内存中 Assembly 对象的引用。HelloGen 测试 hwCompilerParameters 的 GenerateInMemory 属性,如果为 True,就使用反射结果在 ExecuteCompiledAssembly 函数中执行程序。
    类模板和 CodeDOM
    掌握了 CodeDOM 的这些知识,我们在实际示例中来好好应用一下。C# 和 Visual Basic .NET 语言缺乏的一点是对参数多态的支持(通常称为泛型)。一个这样的示例就是 C++ 中的类模板。类模板允许定义这样的类:此类内的某些类型的实例是一般化的,直到类在代码中声明为止。声明时类被给定一种类型,而在编译时,却为应用程序中使用的每种不同的类型创建类。虽然不同编译器可以通过分解类的通用性而在一定程度上优化这个过程,这种通用说明可以满足本文的目标。最好的、最值得一提的这类实现的经典备选者可能就是集合类,因为它们从本质上就是用于通用的目的。
    通过为这些类创建模板,可在一个地方拥有它们的实现,更改模板就改变了所有实例的实现,从中可以受益。因为它们是强类型的,使用时无需使用额外的代码来对变量进行强制转换,这使得代码更清楚、错误更少。用不支持泛型的语言从类模板中受益的一个方法无疑就是使用类型为 Object 的变量来获得本地化的源代码库和通用性。此处的缺点在于类型安全性和对传达给编程人员类型信息的说明性变量名称和代码注释的依赖。
    泛型可能会出现在未来版本的 C# 语言中,现在提议的实现很有前景(参阅 Generics for C# and .NET CLR 和 Future Features FAQ),但在此之前,一种可以提供某些泛型优点的构建类的有益方法会很有帮助。这可通过使用 CodeDOM 创建模板来实现。通过创建这样的类:该类可为集合或其他基于模板的类定义对象图,从而在一个地方拥有实现,您可以从中受益。如果想更改某些内容,只需对模板类进行更改;但需要重新为想要表示的各种类型生成源代码。
    另外,为了不丢失更改,必须注意不能修改先前生成的任一模板类。尽管这是一个严重的不足,但其他基于模板的类生成器都有这种缺陷。然而您确实可从简化转换中获得类型安全性和更清楚的代码。CodeDOM 实现的另一个优点是能够用有 CodeProviders 的多种语言生成这些类,假定已经设计的模板遵循这些语言的约束。
    创建应用程序
    NetTemplateGenerator 是一个通用的应用程序,用来生成 CodeDOM 对象图模板的源代码。这些模板以这样的方式设置:它们可以接受将更改对象图属性的参数,这反过来也可以更改从它们中生成的代码的属性。如果这些参数表示对象图内某些变量的类型,那么我实际上是在模仿 C++ 类模板的行为。NetTemplateGenerator 应用程序本身只负责生成 CodeCompileUnit 对象的源代码,并为用户提供了一种更改模板参数的方法。TypedHashtableProvider 项目公开了一个示例模板 — CTHTGenerator 类,它提供了一种表示简单的 Hashtable 的对象图,Hashtable 的 Key 和 Value 类型可通过 NetTemplateGenerator 进行更改。图 10 显示了加载 TypedHashtableProvider 程序集并更改模板参数后的应用程序。
    使用 CodeDOM 命名空间将模板的威力带到您的 .NET 应用程序中 - 图1
    图 10 模板生成器
    程序集从应用程序主窗体的 Source Assembly 选项卡加载。默认的源程序集指向 TypedHashtableProvider.dll 程序集,它包括用于为该示例创建 CodeCompileUnit 的 CTHTGenerator 类。单击 Load Assembly 按钮将所选的源程序集加载到内存中,使用反射浏览该程序集中的类,如图 11中 PopulateClassesCBO 的代码片段所示。
    NetTemplateGenerator 中定义的 GeneratorMethods 类保留有该类中 MethodInfo 对象的引用,之后可用该类来调用这些方法以设置和检索要生成模板的信息。IsValid 类检查三种 MethodInfo 对象以确保它们符合期望的接口要求。如果是这样,则该对象将被添加到 Generator Class 组合框,可作为有可能生成的新模板用于应用程序中。
    如果在程序集找到一个或多个可接受的类,可以发现 Parameters 面板将由目前选定模板的可替换参数填充(参见图 10)。组合框的 OnChange 事件调用 CreateTypeControls,它使用反射和目前选定组合框引用的 GeneratorMethods 类,从而调用选定类的 QueryVars 方法:

    1. object AppInstance = GeneratorAssembly.CreateInstance(
    2. curMethods.ClassName);
    3. GenClassTypesCol = (NameValueCollection)
    4. curMethods.QueryVarsMethod.Invoke(AppInstance, null);

    该方法返回一个 NameValueTypeCollection 类,该类包括可替换参数名称,以及这些参数的默认值(可选)。该方法的其他部分动态创建窗体上用于输入的 TextBox 控件。
    TypedHashtable 模板允许替换命名空间、类自身的类型名以及存储于 Hashtable 和相关键中值的类型名的值。因为这些类型被表示为字符串,要习惯使用完全限定的类型名称以确保它由 CodeDOM 正确表示。除了用适当的值替换类型名外,可以使用 Class Output 选项卡更改输出参数。这里,选择输出源代码的语言以及存储文件的位置,我从 HelloGen 示例应用程序中借用了 GeneratorOptions 分组框及相关的代码。
    该类的代码在 TemplateGenerator 类的 GenerateCode 方法中生成。该方法首先创建加载程序集的当前选择类的一个实例,并用用户指定的值更新 GenClassTypesCol。然后这些值作为 SetVars 的参数传递回生成类,生成 CodeCompileUnit:

    1. args[0] = GenClassTypesCol;
    2. curMethods.SetVarsMethod.Invoke(AppInstance, args);
    3. ccu = (CodeCompileUnit)curMethods.CCUGeneratorMethod.Invoke(
    4. AppInstance, null);

    使用 CodeCompileUnit,源代码生成为预览文本和文本文件,与 HelloGen 示例中的 GeneratePreview 方法完全一样:

    1. codePreview = new StringWriter();
    2. ntGenerator = GetLanguageGenerator(
    3. (Language)cboTargetLanguage.SelectedIndex);
    4. ntGenerator.GenerateCodeFromCompileUnit(
    5. ccu, codePreview, ntGeneratorOptions);
    6. codeFile = new StreamWriter(txtSourceDirectory.Text);
    7. codeFile.Write(codePreview.ToString());
    8. txtPreview.Text = codePreview.ToString();

    创建模板
    TypedHashtableProvider 项目实现 CTHTGenerator,它负责根据用户选择参数生成简单 Hashtable 模板的 CodeCompileUnit。私有成员变量 TemplateVariables 是一个 NameValueCollection,它保存该信息,并通过 QueryVariables 和 SetVariables 函数在该程序集和 NetTemplateGenerator 程序集之间传递:

    1. public NameValueCollection QueryVariables() {
    2. return TemplateVariables;
    3. }
    4. public void SetVariables(NameValueCollection varCollection) {
    5. TemplateVariables = varCollection;
    6. }

    CTHTGenerator 构造函数将该集合初始化为说明所期望类型的值,各条目名称被窗体用来说明参数(参见图 10)。
    程序集中为 NetTemplateGenerator 使用而设计的任一类必须实现三种方法:QueryVariables、SetVariables 和 GenerateCCU。QueryVariables 只返回用户设置的参数的 NameValueCollection。SetVariables 接受一个类型为 NameValueCollection 的参数,这将把内部集合设置为用户选定的值。最后,GenerateCCU 使用用户提供的参数生成对象图并将 CodeCompileUnit 返回给调用方。
    动态设置对象图中的类型并没有什么魔法。在图 12中您可看到,我使用 CodeDOM 对象构造函数的 String 重载从 NameValueCollection 对象将类型设置为合适的值。
    我选择这部分代码是为了说明类型替换并强调 CodeDOM 中默认属性的表示。创建这一结果的技巧是给定 CodeMemberProperty 对象一个特殊的名称“Item”,并将一个参数添加到 Parameters 集合。除了定义默认属性外,浏览 CTHTGenerator 类的下载代码将发现模板包括下列方法,这些方法是 System.Collections.Hashtable 类中可用方法的一小部分子集:Add、ContainsKey、ContainsValue 和 Remove。有许多属性和方法我并没加到模板中去,您也许希望通过实现这些体验一下 CodeDOM。
    小结
    NetTemplateGen 示例应用程序是使用 CodeDOM 生成基于模板类的一个不错的入门示例,但仍有很多扩展和改进的地方。另一个不错的功能就是在设置单个参数时能够限制您选择的类型。例如,生成用 String 变量作为数学函数操作数的模板类是没有意义的。这可用 XML 传递参数和参数值来实现,使用 XML Schemas 来验证类型。另一个绝好的更改是创建 Visual Studio .NET 外接程序,将该功能合并到 IDE 中。
    相关文章,请参阅:

    Generative Programming:Modern Techniques to Automate Repetitive Programming Tasks
    Generics for C# and .NET CLR
    有关背景信息,请参阅:
    CodeDOM Quick Reference
    System.CodeDom Namespace
    Adam J. Steinert 是 Tara Software 公司的顾问,该公司是位于威斯康星州麦迪逊城的 Yahara Software LLC 公司的子公司。您可以通过 adams@tarasoftware.com 与他联系。