T4 模板

管理 T4 代码生成解决方案中的复杂性

Peter Vogel

 

在我的文章"降低壁垒,代码生成与 T4"4 月发行的 MSDN 杂志 (msdn.microsoft.com/magazine/hh882448),我描述如何 Microsoft 文本模板转换工具包 (T4) 使我们更加容易为开发人员创建代码生成解决方案 (和如何,他们可以开始认识到代码生成的机会)。 然而,与任何编程环境下,一样 T4 解决方案可以成长为复杂的整体解决方案,不能维持或延长。 避免命运需要认识到的 T4 解决方案中的代码可以是重构和集成到代码生成解决方案的各种方式。 这需要一些 T4 代码生成过程的认识。

T4 代码生成过程。

T4 代码生成过程的核心是 T4 引擎,它接受 T4 模板组成的样板代码、 控制块、 类特征和指令。 从这些投入,引擎创建 Microsoft TextTransformation 类继承的临时类 ("生成的转换类")。 然后创建一个应用程序域和域中, 生成的转换类编译和执行以产生的输出,可能是从 C# 程序的 HTML 页的任何东西。

套用文本和模板中的控制块被纳入一个单独的方法 (称为 TransformText) 生成的转换类中。 但是,类特征中的代码 (括在 < # + … … # > 在这种方法不放的分隔符) — — 类功能代码添加到生成的转换类以外的 T4 过程创建的任何方法。 在我以前的文章,例如,我使用类功能块添加 ArrayList — — 外部声明的任何方法 — — 以生成的转换类。 然后,我访问该 ArrayList 中我代码生成控制块作为生成代码的过程的一部分。 除了要添加字段,如 ArrayList,类特征也可以用来添加可从您的代码块中调用的私有方法。

发动机是心脏的过程中,但两个其他组件也参加 T4 代码生成过程:指令处理器和主机。 指令提供基于参数的方法添加代码或以其他方式控制过程。 例如,T4 导入指令有一个 Namespace 的参数创建使用或 Imports 语句中生成的转换类 ; 包含指令已从另一个文件中检索文本,并将其添加到生成的转换类文件的参数。 默认指令处理器处理的 T4 附带的指令。

T4 代码生成过程还需要与引擎执行中的环境集成过程的主机。 这样,并不是所有的程序集生成的转换类代码需求要在模板中指定主机,例如,提供了一套标准的程序集和命名空间的引擎。 请求时,引擎或指令的处理器,该主机将添加引用的程序集 ; 检索 (和有时读取) 文件的发动机 ; 和甚至可以检索自定义指令处理器或指令省略了其参数提供默认值。 主机还提供了生成的转换类中执行,并显示任何错误和警告消息引擎生成的应用程序域。 Visual Studio 可以承载的 T4 引擎 (通过自定义工具 TextTemplatingFileGenerator),可以处理 T4 模板以外的 Visual Studio TextTransform 命令行实用程序。

作为这一进程,以看看静态代码,结合 T4 模板的示例控制块和一类特征所示图 1

图 1 示例 T4 模板

<#@ template language="VB" #>
public partial class ConnectionManager
{
<#
  For Each conName As String in Connections
#>
  private void <#= conName #>(){}
<#
  Next
#>
<#+
  Private Function GetFormattedDate() As String
    Return DateTime.Now.ToShortDateString()
  End Function
#>

生成的转换类看起来是什么所示图 2

图 2 生成的转换类

Public Class GeneratedTextTransformation
  Inherits Microsoft.VisualStudio.TextTemplating.TextTransformation
  Public Overrides Function TransformText() As String
    Me.Write("public partial class ConnectionManager{")
    For Each conName As String in Connections
      Me.Write("private void ")
      Me.Write(Me.ToStringHelper.ToStringWithCulture(conName))
      Me.Write("(){}")    
    Next
  End Function
  Private Function GetFormattedDate() As String
    Return DateTime.Now.ToShortDateString()
  End Fuction
End Class

鉴于此 T4 代码生成过程的说明,可以进更多维护的组件使用这三个选项的任何重构可能整体解决方案:

  1. 类特征
  2. 扩展 TextTransformation 基类
  3. 自定义指令处理器

这些机制还允许您在多个代码生成解决方案之间重用代码。 若要展示这些选项,我将使用一个平凡简单案例研究:添加到生成的代码的版权声明。 "Meta 岬"的编写代码生成的代码创建足够的复杂性而不摘复杂的案例研究 — — 你可以在你自己。

至少,我不是讨论的另一个选择是有的:开发自己的主机,所以您可以从您的 T4 模板调用特定主机的功能。 只有你打算调用从外部 Visual Studio T4 处理,不想使用 TextTransform 命令行工具,创建一个新的主机是真的有必要的。

类特征

类功能提供了最简单的方法,减少您的 T4 过程中的复杂性和重用代码。 类功能使您可以将您的代码生成过程的部件封装到可以从表单"主线代码"的 TransformText 方法调用的方法。 您还可以利用在多个解决方案之间重用类特征包含指令。

使用类功能的第一步是添加到您的项目包含您希望在多个代码生成解决方案,括在 T4 中重用的类特征的 T4 文件 < # + … … # > 分隔符。 这可以包括方法、 属性和字段。 你的模板文件,也可以包含 T4 特定的功能,例如,指令。 因为您的文件仍然是 T4 文件,它将生成将被编译到您的应用程序,因此您应禁止该文件的代码生成的代码。 这样做最简单的方法是清除您的类特征文件的自定义工具属性。 此示例定义了一个称为 ReturnCopyright 作为一类的功能,在 Visual Basic 中写入方法:

<#+
 Public Function ReturnCopyright() As String
   Return "Copyright by PH&V Information Services, 2012"
 End Function
#>

您已经定义了一类的功能,可以将它添加到使用包含指令和从控件功能阻止 T4 模板中使用的模板。 下一个示例中,假定以前类功能定义一个名为 CopyrightFeature.tt 的文件中,使用 ReturnCopyright 方法作为表达式:

<#@ Template language="VB"   #>
<#@ Output extension=".generated.cs" #>
<#= ReturnCopyright() #>
<#@ Include file="CopyrightFeature.tt" #>

或者,只是可以使用的 T4 正常样板语法生成类似的代码:

<#+
  Public Function ReturnCopyright() As String
#>
  Copyright by PH&V Information Services, 2012
<#+
  End Function
#>

您还可以使用写入和 WriteLine 方法在您的类特征内生成的代码,一样,此类功能:

<#+
  Public Sub WriteCopyright()
    Write("Copyright by PH&V Information Services, 2012")
  End Function
#>

后者的两种方法将使用下面的代码来调用方法,一旦一个包含指令添加到模板:

<# WriteCopyright() #>

你可以参数化类功能使用正常的函数的参数和菊花链功能一起使用包含指令本身结构完善、 可重复使用的库建立其他 T4 文件中包含的 T4 文件中。

与其他的解决方案相比,使用类特征有至少一个优点和一个缺点。 开发您的代码生成解决方案时,您会遇到好处:类的功能 (与一般包括) 不涉及编译的程序集。 出于性能原因,T4 引擎可能会锁定它使用时,它将生成的转换类加载到其应用程序域的程序集。 这意味着测试和修改已编译的代码时,您可能会发现你不能取代有关程序集而不关闭并重新启动 Visual Studio。 这类特征不会发生。 请注意这已解决在 Visual Studio 2010 SP1,不再锁定的程序集。

开发人员可能会遇到的缺点,但是,当他们使用您的代码生成解决方案时:他们必须将不只是您 T4 模板添加到他们的项目,但也都支持 T4 包括文件。 这是一种情况,您可以考虑创建一个包含所有开发人员需要为您的代码生成解决方案,因此可以将它们作为一个组进行添加的 T4 文件的 Visual Studio 模板。 它还会有意义将包括文件隔离到解决方案中的某个文件夹。 下面的示例使用包含指令添加一个包含类特征称为模板的文件夹中的文件:

<#@ Include file="Templates\classfeatures.tt" #>

你并不,当然,限于只在包含文件中使用类特征 — — 您可以包括任意一组控制块和您想要将纳入 TransformText 方法生成的转换类的文本。 不过,如避免 GoTos,使用明确定义的成员中包含文件可以帮助管理您的解决方案的概念的复杂性。

扩展 TextTransformation 类

TextTransformation 类替换为您自己的类中,可以包含自定义功能,可以在生成的转换类中调用该类中的方法。 扩展 TextTransformation 类是不错的选择,将在很多人 (或所有) 您的代码生成解决方案中使用的代码时:实质上,出您的解决方案,为 T4 发动机因子对该代码。

扩展 TextTransformation 类的第一步是创建一个从类继承的抽象类:

public abstract class PhvisT4Base:
  Microsoft.VisualStudio.TextTemplating.TextTransformation
{
}

如果您没有包含 TextTransformation 类的 Microsoft.VisualStudio.TextTemplating DLL,请下载您的 Visual Studio 版本的 SDK 之前添加引用。

根据您的 T4 的版本,可能必须提供作为一部分继承从 TextTransformation 的 TransformText 方法的实现。 如果这样,您的方法必须返回包含生成的转换类,举行 TextTransformation 类生成的输出的字符串­环境属性。 重写的 TransformText 方法,如有需要,应该像下面这样:

public override string TransformText()
{
  return this.GenerationEnvironment.ToString();
}

使用类的功能,如有两个选项用于将代码添加到生成的转换类。 此示例中一样,您可以创建返回字符串的值的方法:

protected string Copyright()
{
  return @"Copyright PH&V Information Services, 2012";
}

在表达式或 T4 写或 WriteLine 方法,如这些示例中,可以使用此方法:

<#= CopyRight() #>
<#  WriteLine(CopyRight()); #>

或者,您可以使用 TextTransformation 基类写或 WriteLine 方法直接在您将添加到 TextTransformation 基类的方法一样,此示例:

protected void Copyright()
{
  base.Write(@"Copyright PH&V Information Services, 2012");
}

然后可以调用此方法,从控制块,像这样:

<# CopyRight(); #>

替换默认的 TextTransformation 类的最后一步是指定您生成的转换类是从您新的类继承的 T4 模板中。 你这样的模板属性的 Inherits 参数:

<#@ Template language="C#" inherits="PhvT4Utils.PhvisT4Base" #>

此外必须向您的 T4 模板添加引用的 DLL 包含基类,使用 dll 的完整物理路径名称的程序集指令:

<#@ Assembly name="C:\T4Support\PhvT4Utils.dll" #>

或者,您可以添加您的基类全局程序集缓存 (GAC 中)。

在 Visual Studio 2010 年可以使用环境和宏变量简化该 DLL 的路径。 例如,在开发期间,可以使用 $(ProjectDir) 宏引用包含基类的 DLL:

<#@ Assembly name="$(ProjectDir)\bin\PhvT4Utils.dll" #>

在运行时,假设您安装程序文件的文件夹,您的类文件中,您可以使用 32 位版本的 Windows,或上 64 位版本、 %programw6432%为程序文件文件夹中或为程序文件 %programfiles(x86)%的环境变量 %programfiles%(86) 文件夹。 本示例假定该 DLL 有 64 位版本的 Windows 上被放在 Files\PHVIS\T4Tools 或 java 命令:

<#@ Assembly name="%ProgramW6432%\PHVIS\T4Tools\PHVT4Utils.DLL" #>

作为与其他解决方案的已编译的代码,Visual Studio 可能锁定您的代码包含在生成的转换类的执行过程中的 DLL。 如果正在开发您的 TextTransformation 类时,将发生这种情况,您需要重新启动 Visual Studio — — 并重新编译您的代码 — — 你可以进行更多更改之前。

侧栏:警告和错误

鲁棒的任何进程方面的进展提供反馈。 在一类的功能或扩展 TextTransformation 类时,您可以通过添加调用 TextTransformation 类错误和警告方法报告生成的转换类的执行中的问题。 这两种方法接受单个字符串输入并将该字符串传递到 Microsoft 文本模板转换工具包 (T4) 主机 (主机是负责决定如何显示消息)。 在 Visual Studio 中,在错误列表窗口中显示消息。

此示例报告类功能中的一个错误:

< # = GetDatabaseData("") # >
< # + 专用的字符串 (字符串 ConnectionString) GetDatabaseData
{
如果 (ConnectionString = ="")
    {
基地。错误 ("无连接字符串提供。") ;
    }
  返回"";
    }
...

在类的功能中,错误和警告不能用于报告在 T4 过程中,仅在生成的转换类执行时出现的错误的问题。 把它放到另一种方式,在类中的功能,将出现错误和警告消息才可正确装配生成的转换类的 T4 文件中的数据,编译和执行。

如果您正在创建自定义的指令处理器,您仍可以 T4 错误报告通过添加编译器的过程与集成­错误对象类错误的属性。 CompilerError 对象支持传递多个片段中的错误 (包括行号和文件名称),有关的信息,但此示例只是将 ErrorText 属性设置:

System.CodeDom.Compiler.CompilerError err 新 =
  System.CodeDom.Compiler.CompilerError() ;
错了。ErrorText ="失踪指令参数";
这。Errors.Add(err) ;

          — — 联合

自定义指令处理器

管理代码生成过程中的复杂性的最强大和最灵活的方法是使用自定义指令的处理器。 以及其它功能,指令处理器可以将引用添加到生成的转换类,并将代码插入到执行之前和之后生成的转换类 TransformText 方法的方法。 指令也容易让开发者使用:他们只需要为该指令参数提供值,然后使用指令使可用的成员。

指令处理器最关键问题是您必须添加一个密钥,以便开发人员可以使用您的处理器的 Windows 注册表中。 在这里,再次,值得考虑打包您的 T4 解决方案,以便可以自动作出的注册表项。 32 位版本的 Windows 中,密钥必须:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\
  visualstudioversion\TextTemplating\DirectiveProcessors

对于 64 位版本的 Windows,关键是:

HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\
  VisualStudio\10.0\TextTemplating\DirectiveProcessors

您的名称是密钥的您指令处理器类名称,其中,以下 Microsoft 的命名约定,应该结束与"DirectiveProcessor"。你需要以下子项:

  • 默认值:为空或你的指令的说明。
  • 类:您的格式为 Namespace.ClassName 的类的全名。
  • 代码程序集/库:如果您已经在 GAC (程序集) 或您指令 DLL (基本代码) 的完整路径中放置您指令 DLL DLL 的名称。

在开发时您的处理器,如果你没有此项权利,Visual Studio 将生成一个错误它无法找到您的指令处理器或解决其类型。 修复您的注册表项中的任何错误后,您可能需要重新启动 Visual Studio 来接您的更改。

若要使用您的指令处理器,开发人员将指令与任何名称添加到 T4 模板和关系要您通过该指令处理器参数 (它引用的 Windows 注册表项) 的处理器的指令。 T4 模板执行时,您的指令处理器被通过开发人员与任何参数一起使用开发人员指定的指令的名称。

此示例联系到一个称为 CopyrightDirectiveProcessor 的处理器命名版权的指令,并包括称为一年的参数:

<#@ Copyright Processor="CopyrightDirectiveProcessor" Year=”2012” #>

与一类的功能,如从指令的处理器输出添加到生成的转换类以外的 TransformText 方法。 因此,您将使用您的处理器将新成员添加到生成的转换类的发展­opers 可以在其模板控制块中使用。 前一示例指令可能已添加属性或开发人员可以使用一个字符串变量在表达式中,像这样:

<#= Copyright #>

当然下, 一步是要创建指令,该指令处理过程。

创建自定义指令处理器

T4 指令处理器从 Microsoft.VisualStudio.TextTemplating.DirectiveProcessor (下载获取 TextTemplating 库的视觉工作室 SDK) 的类继承。 您的指令处理器必须,从它的 GetClassCodeForProcessingRun 方法返回将被添加到生成的转换类的代码。 但是,在调用 GetClassCodeForProcessingRun 方法前, T4 引擎将调用的处理器 IsDirective­支持的方法 (传递你的指令的名称) 和 ProcessDirective 方法 (传递指令的名称和其参数的值)。 从 IsDirectiveSupported 的方法,您应该返回 false 如果你指令不应该继续执行,而真正的否则。

因为 ProcessDirective 方法传递指令,这是通常生成的代码,GetClassCodeForProcessingRun 将返回有关的所有信息。 您可以提取通过阅读它们从该方法的第二个参数 (称为参数) 在指令中指定的参数的值。 此代码在 ProcessDirective 方法中,为参数调用一年看起来,并使用它来建立一个字符串,包含变量的声明。 从 GetClassCodeForProcessingRun,然后返回的字符串:

string copyright = string.Empty;
public override void ProcessDirective(
  string directiveName, IDictionary<string, string> arguments)
{
  copyright = "string copyright " +
              "= \"Copyright PH&V Information Services, " +
              arguments["Year"] +"\";";
}
public override string GetClassCodeForProcessingRun()
{
  return copyright;
}

引用和使用,还可以将您的指令处理器添加 /­导入到支持 GetClassCodeForProcessingRun 通过添加的代码生成的转换类的语句。 若要添加对生成的转换类的引用,您只需要从 GetReferencesForProcessingRun 方法返回的字符串数组中的库的名称。 如果,例如,添加到生成的转换类的代码需要组成 System.XML 命名空间的类,您会使用这样的代码:

public override string[] GetReferencesForProcessingRun()
{
  return new string[] {"System.Xml"};
}

同样,您可以通过从 GetImportsForProcessingRun 方法返回一个字符串数组,指定要添加到 (作为使用或 Imports 语句) 生成的转换类的命名空间。

生成的代码转换类还包括前和调用 TransformText 方法之前的 post-initialization 方法。 您可以返回从 GetPreInitializationCodeForProcessingRun 和 GetPostInitializationCodeForProcessingRun 方法将被添加到这些方法的代码。

调试时,请记住使用运行自定义工具不会导致 Visual Studio 建立您的解决方案。 您的指令处理器中进行更改时,您需要构建您的解决方案,在执行您的模板之前拿起您的最新更改。 而且,再次,因为 T4 锁定它使用的程序集,您可能会发现您必须重新启动 Visual Studio,并重新编译您的代码,再进行测试。

T4 显著降低了用于将代码生成集成到您的工具包的障碍。 然而,作为任何其他应用程序,您需要考虑如何你会建筑师完整的解决方案,以支持可维护性和可扩展性。 T4 代码生成过程提供了几个地方 — — 每个都有其自己的成本和效益 — — 你可以在这里重构您的代码并将其插入到该进程。 无论您使用哪个机制,您可以确保在您的 T4 解决方案是 well-architected。

侧栏:在运行时使用 T4

你可以利用进行预处理的文本模板,就可以在运行时生成的文本生成的转换类看起来像一瞥。 编译或执行运行时 Microsoft 文本模板转换工具包 (T4) 代码生成的结果可能不是大多数开发人员想要解决的东西。 但是,如果您需要的 XML 或 HTML 文档的几个"类似,但不同"版本 (或某些其它文本),进行预处理文本模板允许您使用 T4 在运行时生成这些文件。

作为与其他 T4 解决方案,在运行时使用 T4 的第一步是 T4 文件添加到项目,从新建项目对话框。 但是,添加文本模板文件,而不是,您将添加预处理文本模板 (也在 Visual Studio 新项对话框中列出)。 进行预处理的文本模板相同标准的 T4 文本模板文件,只是自定义工具的属性设置为 TextTemplatingFileProcessor,而不是通常的 TextTemplatingFileGenerator。

不同于与文本模板,包含生成的代码进行预处理的文本模板中的子文件不包含输出代码生成的转换类的。 相反,该文件包含这东西看起来很像您生成的转换类之一:具有的一种方法进行预处理文本模板文件的名称相同的类称为 TransformText。 作为字符串返回运行的时,在调用该 TransformText 方法,你所期待的 T4 模板代码文件中找到:生成的代码。 所以,对于进行预处理文本模板文件,称为 GenerateHTML,会检索及其生成的文本,在运行时使用这样的代码:

GenerateHTML HtmlGen = 新的 GenerateHTML() ;
html 字符串 = HtmlGen.TransformText() ;

      — 联合

Peter Vogel 是在 ph 值及 V 信息服务的主体。他最后一本书是"在实际代码的生成。NET"(艾迪生 - 韦斯利专业,2010年)。Ph 值和 V 信息服务专业在便利的基于服务的体系结构设计和集成。到这些体系结构的网络技术。在他的咨询实践,Vogel 写了学习树国际面向服务的体系结构设计课程,教导全世界。

多亏了以下的技术专家审查这篇文章:Gareth Jones