使用T4模板生成设计时代码

使用设计时 T4 文本模板,你可以在 Visual Studio 项目中生成程序代码和其他文件。  通常,你编写一些模板,以便它们根据来自模型的数据来改变所生成的代码。  模型是包含有关应用程序要求的关键信息的文件或数据库。  

1.创建设计时 T4 文本模板

在 Visual Studio 中创建设计时 T4 模板

  1. 创建一个 Visual Studio 项目或打开一个现有项目。

例如,在“文件”菜单上,选择“新”“项目”

  1. 向你的项目添加文本模板文件,并给予其带有扩展名 .tt 的名称。

若要执行此操作,在“解决方案资源管理器”中,在你的项目的快捷菜单上,选择“添加”“新建项”。  在“添加新项”对话框的中间窗格选择“文本模板”。  

请注意,该文件的“自定义工具”属性为“TextTemplatingFileGenerator”

  1. 打开该文件。  该文件中已包含下列指令:  

4. <#@ template hostspecific="false" language="C#" #>

5. <#@ output extension=".txt" #>

如果已将模板添加到 Visual Basic 项目,则语言特性将为“VB”。

  1. 在文件末尾添加一些文本。  例如:  

7. Hello, world!

  1. 保存该文件。

你可能会看到一个“安全警告”消息框,要求确认要运行该模板。  单击“确定”。  

  1. “解决方案资源管理器”中,展开模板文件节点,你将找到带有扩展名 .txt 的文件。  该文件包含从该模板生成的文本。  

生成可变文本

通过文本模板,可以使用程序代码更改已生成文件的内容。

使用程序代码生成文本

1.     更改 .tt 文件的内容:

<#@ template hostspecific="false" language="C#" #>
<#@ output extension=".txt" #>
<#int top = 10;
 
for (int i = 0; i<=top; i++) 
{ #>
   The square of <#= i #> is <#= i*i #>
<# } #>

2.     保存 .tt 文件,然后重新检查已生成的 .txt 文件。  该文件列出数字 0 到 10 的平方。  

请注意,语句括在 <#...#> 内,单个表达式括在 <#=...#> 内。 如果在 Visual Basic 中编写生成代码,则 template 指令应包含 language="VB"  默认为 "C#"  

调试设计时 T4 文本模板

创建文本模板:

·          debug="true" 插入 template 指令。  例如:  

<#@ template debug="true" hostspecific="false" language="C#" #>

·         在模板中使用为普通代码设置断点的相同方式设置断点。

·         在“解决方案资源管理器”中,从文本模板文件的快捷菜单选择“调试 T4 模板”

该模板将运行并在断点处停止。  你可以以常用方式检查变量并逐步执行代码。  

结构化文本模板

作为一种良好做法,我们往往将模板代码分成两部分:

·         配置或数据收集部分,它在变量中设置值,但不包含文本块。  在上一个示例中,此部分是 properties 的初始化。  

此部分有时称为“模型”部分,因为它会构造一个存储内模型,并且通常读取模型文件。

·         文本生成部分(示例中的 foreach(...){...}),它使用变量的值。

虽然这不是必要的分离,但是通过这种方式可以降低包括文本的部分的复杂性,从而更便于读取模板。

读取文件或其他源

若要访问模型文件或数据库,模板代码可以使用诸如 System.XML 之类的程序集。  若要获取对这些程序集的访问权限,必须插入如下指令:  

<#@ assembly name="System.Xml.dll" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.IO" #>

assembly 指令使指定的程序集可供模板代码使用,方式与 Visual Studio 项目中的“引用”部分相同。  你无需包括对 System.dll 的引用,它是自动引用的。   import 指令允许你使用类型而不使用其完全限定名,方式与普通程序文件中的 using 指令相同。  

例如,导入 System.IO 之后,可以编写:

C#

          <# var properties = File.ReadLines("C:\\propertyList.txt");#>
...
<# foreach (string propertyName in properties) { #>
...

通过相对路径名打开文件

若要从相对于文本模板的位置加载文件,可以使用 this.Host.ResolvePath()  若要使用 this.Host,你必须在 hostspecific="true" 中设置template  

<#@ template debug="false" hostspecific="true" language="C#" #>
 

然后你可以进行编写,例如:

C#

<# string fileName = this.Host.ResolvePath("filename.txt");
 string [] properties = File.ReadLines(filename);
#>
...
<#  foreach (string propertyName in properties { #>
...
 

还可以使用 this.Host.TemplateFile,它标识当前模板文件的名称。

this.Host 的类型(在 VB 中是 Me.Host)是 Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost

Visual Studio 获取数据

若要使用 Visual Studio 中提供的服务,请设置 hostSpecific 特性并加载 EnvDTE 程序集。  然后,你可以使用 IServiceProvider.GetCOMService() 访问 DTE 和其他服务。  例如:  

scr

<#@ template hostspecific="true" language="C#" #>
<#@ output extension=".txt" #>
<#@ assembly name="EnvDTE" #>
<#
  IServiceProvider serviceProvider = (IServiceProvider)this.Host;
  EnvDTE.DTE dte = (EnvDTE.DTE) serviceProvider.GetCOMService(typeof(EnvDTE.DTE));
#>
 
Number of projects in this VS solution:  <#= dte.Solution.Projects.Count #>

自动重新生成代码

通常,Visual Studio 解决方案中的多个文件都使用一个输入模型生成。  每个文件从其自己的模板生成,但这些模板全都引用同一个模型。  

如果源模型发生更改,则应重新运行该解决方案中的所有模板。  若要手动执行此操作,请选择“生成”菜单上的“转换所有模板”  

如果已安装 Visual Studio 可视化和建模 SDK,则可以在每次执行生成时自动转换所有模板。  为此,可在文本编辑器中编辑项目文件(.csproj 或 .vbproj),然后在文件末尾附近(其他任何 <import> 语句之后)添加以下行:  

<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v11.0\TextTemplating\Microsoft.TextTemplating.targets" />
<PropertyGroup>
   <TransformOnBuild>true</TransformOnBuild>
   <!-- Other properties can be inserted here -->
</PropertyGroup>

错误报告

若要在 Visual Studio 错误窗口中放置错误消息和警告消息,可以使用以下方法:

Error("An error message");
Warning("A warning message");

将现有文件转换为模板

模板的一个非常有用的特性是:它们看起来与其生成的文件(加上一些插入的程序代码)非常相似。  这暗示了创建模板的一种有用方法。  首先,创建一个普通的文件(如 Visual C# 文件)作为原型,然后逐步引入可更改所生成文件的生成代码。  

将现有文件转换为设计时模板

1.     对于你的 Visual Studio 项目,添加要生成的类型的文件,例如 .cs.vb  .resx 文件。

2.     测试新文件以确保其工作。

3.     在解决方案资源管理器中,将文件扩展名更改为 .tt

4.     验证 .tt 文件的以下属性:

自定义工具 =

TextTemplatingFileGenerator

生成操作 =

5.     在文件开头插入以下行:

6.      <#@ template debug="false" hostspecific="false" language="C#" #>
7.      <#@ output extension=".cs" #>

如果要以 Visual Basic 编写模板的生成代码,请将 language 特性设置为 "VB",而不是 "C#"

 extension 特性设置为要生成的文件类型的文件扩展名,例如 .cs.resx  .xml

8.     保存该文件。

将使用指定扩展名创建一个附属文件。  该文件对于相应文件类型具有正确的属性。  例如,.cs 文件的“生成操作”属性将为“编译”  

验证生成的文件是否包含与原始文件相同的内容。

9.     确定要更改的文件部分。  例如,一个仅在特定条件下显示的部分、一个重复的部分或特定值会有所变化的部分。  插入生成代码。  保存该文件,然后验证附属文件是否正确生成。  重复此步骤。  

2. 演练:使用文本模板生成代码

通过代码生成,可以生成在源模型更改时能够轻松更改的强类型程序代码。  将此方法与编写完全通用程序(接受配置文件)的备选方法对比,可以发现后者虽然更灵活,但导致代码既没有这么容易读取和更改,也没有这么好的性能。  本演练演示了此优势。  

用于读取 XML 的指定类型化代

System.Xml 命名空间提供全面的工具,用于加载 XML 文档,然后在内存中自由导航该文档。  不过,所有节点都具有相同的类型 XmlNode。  因此,很容易造成编程错误,例如出现错误类型的子节点或错误特性。  

在此示例项目中,模板读取示例 XML 文件,然后生成与每种节点类型对应的类。  在手写代码中,可以使用这些类导航 XML 文件。  还可以针对使用相同节点类型的其他任何文件运行应用程序。  此示例 XML 文件的作用是提供您希望应用程序处理的所有节点类型的示例。  

说明: System_CAPS_note注意

Visual Studio 附带的应用程序 xsd.exe 可以从 XML 文件生成强类型的类。  此处显示的模板作为示例提供。  

下面是示例文件:

<?xml version="1.0" encoding="utf-8" ?>
<catalog>
  <artist id ="Mike%20Nash" name="Mike Nash Quartet">
    <song id ="MikeNashJazzBeforeTeatime">Jazz Before Teatime</song>
    <song id ="MikeNashJazzAfterBreakfast">Jazz After Breakfast</song>
  </artist>
  <artist id ="Euan%20Garden" name="Euan Garden">
    <song id ="GardenScottishCountry">Scottish Country Garden</song>
  </artist>
</catalog>

在本演练构造的项目中,您可以编写如下代码,在键入时 IntelliSense 会提示正确的特性和子名称:

Catalog catalog = new Catalog(xmlDocument);
foreach (Artist artist in catalog.Artist)
{
  Console.WriteLine(artist.name);
  foreach (Song song in artist.Song)
  {
    Console.WriteLine("   " + song.Text);
  }
}

将此代码与不使用模板编写的非类型化代码对比:

XmlNode catalog = xmlDocument.SelectSingleNode("catalog");
foreach (XmlNode artist in catalog.SelectNodes("artist"))
{
    Console.WriteLine(artist.Attributes["name"].Value);
    foreach (XmlNode song in artist.SelectNodes("song"))
    {
         Console.WriteLine("   " + song.InnerText);
     }
}

在强类型版本中,对 XML 架构进行更改将导致对类进行更改。  编译器将突出显示必须更改的应用程序代码部分。  在使用通用 XML 代码的非类型化版本中,则没有此类支持。  

在此项目中,使用一个模板文件来生成实现类型化版本的类。

设置项

创建或打开一个 C#

可以将此方法应用于任何代码项目。  本演练使用的是一个 C# 项目,并且出于测试目的,我们使用了一个控制台应用程序。  

创建项目

1.     “文件”菜单上,单击“新建”,然后单击“项目”

2.     单击“Visual C#”节点,然后在“模板”窗格中,单击“控制台应用程序”

将原型 XML 文件添加到项

此文件的作用是提供您希望应用程序能够读取的 XML 节点类型的示例。  它可以是一个将用于测试应用程序的文件。  模板将为此文件中的每个节点类型生成一个 C# 类。  

此文件应是项目的一部分以便模板能够读取,但不会内置到编译的应用程序中。

添加 XML 文件

1.     “解决方案资源管理器”中右击项目,单击“添加”,然后单击“新建项”

2.     “添加新项目”对话框的“模板”窗格中,选择“XML 文件”

3.     将示例内容添加到文件中。

4.     对于本演练,将文件命名为 exampleXml.xml  将该文件的内容设置为上一节中显示的 XML。  

..

添加测试代码文

将一个 C# 文件添加到项目中,然后在其中写入您希望能够编写的代码示例。  例如:  

using System;
namespace MyProject
{
  class CodeGeneratorTest
  {
    public void TestMethod()
    {
      Catalog catalog = new Catalog(@"..\..\exampleXml.xml");
      foreach (Artist artist in catalog.Artist)
      {
        Console.WriteLine(artist.name);
        foreach (Song song in artist.Song)
        {
          Console.WriteLine("   " + song.Text);
} } } } }

在此阶段,该代码将无法编译。  编写模板时,将生成允许成功进行编译的类。  

一个更全面的测试可以根据示例 XML 文件的已知内容检查此测试函数的输出。  但在本演练中,我们只要求此测试方法能够编译。  

添加文本模板文

添加文本模板文件,然后将输出扩展名设置为“.cs”。

将文本模板文件添加到项目

1.     “解决方案资源管理器”中右击项目,单击“添加”,然后单击“新建项”

2.     “添加新项目”对话框的“模板”窗格中,选择“文本模板”

说明: System_CAPS_note注意

确保添加的是文本模板,而不是预处理文本模板。

3.     在该文件的模板指令中,将 hostspecific 特性更改为 true

此更改将使模板代码能够获取对 Visual Studio 服务的访问。

4.     在输出指令中,将扩展特性更改为“.cs”,以便模板生成一个 C# 文件。  在 Visual Basic 项目中,应将其更改为“.vb”。  

5.     保存该文件。  在此阶段,该文本模板文件应包含以下行:  

6. <#@ template debug="false" hostspecific="true" language="C#" #>
7. <#@ output extension=".cs" #>

.

请注意,.cs 文件在解决方案资源管理器中显示为模板文件的附属文件。  可通过单击模板文件名称旁的 [+] 查看该文件。  每当保存模板文件或将焦点从该模板文件移开时,将从该模板文件生成此文件。  生成的文件将作为项目的一部分编译。  

为方便起见,在开发模板文件时,应排列模板文件和生成文件的窗口,以便它们可以相邻显示。  这将允许您立即查看模板的输出。  您还会注意到当模板生成无效的 C# 代码时,错误消息窗口中将显示错误。  

保存模板文件时,直接在生成的文件上执行的任何编辑都将丢失。  因此,应避免编辑生成的文件,或仅编辑该文件进行短期实验。  有时,当 IntelliSense 在操作中时,在生成的文件中尝试一小段代码,然后将其复制到模板文件,这会很有用。  

开发文本模

遵循对敏捷开发的最佳建议,我们将分小步骤开发模板,清除逐渐产生的一些错误,直到测试代码可以正确编译和运行。

确定要生成的代码的原

测试代码要求该文件中的每个节点都有一个类。  因此,如果将以下行附加到模板中,然后保存该模板,则一些编译错误将会消失:  

class Catalog {} 
class Artist {}
class Song {}

这可以帮助您了解所需的内容,但应从示例 XML 文件的节点类型生成声明。  从模板中删除这些实验行。  

从模型 XML 文件生成应用程序代

若要读取 XML 文件并生成类声明,请将模板内容替换为以下模板代码:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml"#>
<#@ import namespace="System.Xml" #>
<#
 XmlDocument doc = new XmlDocument();
 // Replace this file path with yours:
 doc.Load(@"C:\MySolution\MyProject\exampleXml.xml");
 foreach (XmlNode node in doc.SelectNodes("//*"))
 {
#>
  public partial class <#= node.Name #> {}
<#
 }
#>

将文件路径替换为项目的正确路径。

请注意代码块分隔符 <#...#>  这些分隔符将生成文本的程序代码片段括起来。  表达式块分隔符 <#=...#> 将一个可以计算为字符串的表达式括起来。  

编写可以生成应用程序源代码的模板时,您在处理两个单独的程序文本。  每次保存模板或将焦点移至其他窗口时,该代码块分隔符内的程序都会运行。  该程序生成的文本(显示在分隔符外)将复制到生成的文件中,并成为应用程序代码的一部分。  

<#@assembly#> 指令的行为方式类似于引用,使程序集可供模板代码使用。  通过模板可以看到的程序集列表与应用程序项目中的引用列表相分离。  

<#@import#> 指令的行为方式类似于 using 语句,允许您在导入的命名空间中使用类的短名称。

遗憾的是,虽然此模板可以生成代码,但是它为示例 XML 文件中的每个节点都生成一个类声明,这样,当 <song> 节点存在多个实例时,将会出现类 song 的多个声明。

读取模型文件,然后生成代

许多文本模板都遵循下面一种模式:模板的第一部分读取源文件,第二部分生成模板。  我们需要读取所有示例文件以汇总其包含的节点类型,然后生成类声明。  需要另一个 <#@import#>,以便我们可以使用 Dictionary<>:  

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml"#>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Collections.Generic" #>
<#
 // Read the model file
 XmlDocument doc = new XmlDocument();
 doc.Load(@"C:\MySolution\MyProject\exampleXml.xml");
 Dictionary <string, string> nodeTypes = 
        new Dictionary<string, string>();
 foreach (XmlNode node in doc.SelectNodes("//*"))
 {
   nodeTypes[node.Name] = "";
 }
 // Generate the code
 foreach (string nodeName in nodeTypes.Keys)
 {
#>
  public partial class <#= nodeName #> {}
<#
 }
#>

添加辅助方

类功能控制块是一个可以在其中定义辅助方法的块。  该块以 <#+...#> 分隔,并且必须作为文件中的最后一个块显示。  

如果您更希望类名以大写字母开始,则可以将模板的最后一部分替换为以下模板代码:

// Generate the code
 foreach (string nodeName in nodeTypes.Keys)
 {
#>
  public partial class <#= UpperInitial(nodeName) #> {}
<#
 }
#>
<#+
 private string UpperInitial(string name)
 { return name[0].ToString().ToUpperInvariant() + name.Substring(1); }
#>

在此阶段,生成的 .cs 文件包含以下声明:

public partial class Catalog {}
public partial class Artist {}
public partial class Song {}

可以使用相同的方法添加更多详细信息,如子节点的属性、特性和内部文本。

访问 Visual Studio API

通过设置 <#@template#> 指令的 hostspecific 特性,可以允许模板获取对 Visual Studio API 的访问。  模板可以使用此功能获取项目文件的位置,以避免在模板代码中使用绝对文件路径。  

<#@ template debug="false" hostspecific="true" language="C#" #>
...
<#@ assembly name="EnvDTE" #>
...
EnvDTE.DTE dte = (EnvDTE.DTE) ((IServiceProvider) this.Host)
                       .GetService(typeof(EnvDTE.DTE));
// Open the prototype document.
XmlDocument doc = new XmlDocument();
doc.Load(System.IO.Path.Combine(dte.ActiveDocument.Path, "exampleXml.xml"));

完成文本模

以下模板内容生成允许测试代码编译和运行的代码。

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="EnvDTE" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Collections.Generic" #>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
namespace MyProject
{
<#
 // Map node name --> child name --> child node type
 Dictionary<string, Dictionary<string, XmlNodeType>> nodeTypes = new Dictionary<string, Dictionary<string, XmlNodeType>>();
 
 // The Visual Studio host, to get the local file path.
 EnvDTE.DTE dte = (EnvDTE.DTE) ((IServiceProvider) this.Host)
                       .GetService(typeof(EnvDTE.DTE));
 // Open the prototype document.
 XmlDocument doc = new XmlDocument();
 doc.Load(System.IO.Path.Combine(dte.ActiveDocument.Path, "exampleXml.xml"));
 // Inspect all the nodes in the document.
 // The example might contain many nodes of the same type, 
 // so make a dictionary of node types and their children.
 foreach (XmlNode node in doc.SelectNodes("//*"))
 {
   Dictionary<string, XmlNodeType> subs = null;
   if (!nodeTypes.TryGetValue(node.Name, out subs))
   {
     subs = new Dictionary<string, XmlNodeType>();
     nodeTypes.Add(node.Name, subs);
   }
   foreach (XmlNode child in node.ChildNodes)
   {
     subs[child.Name] = child.NodeType;
   } 
   foreach (XmlNode child in node.Attributes)
   {
     subs[child.Name] = child.NodeType;
   }
 }
 // Generate a class for each node type.
 foreach (string className in nodeTypes.Keys)
 {
    // Capitalize the first character of the name.
#>
    partial class <#= UpperInitial(className) #>
    {
      private XmlNode thisNode;
      public <#= UpperInitial(className) #>(XmlNode node) 
      { thisNode = node; }
 
<#
    // Generate a property for each child.
    foreach (string childName in nodeTypes[className].Keys)
    {
      // Allow for different types of child.
      switch (nodeTypes[className][childName])
      {
         // Child nodes:
         case XmlNodeType.Element:
#>
      public IEnumerable<<#=UpperInitial(childName)#>><#=UpperInitial(childName) #>
      { 
        get 
        { 
           foreach (XmlNode node in
                thisNode.SelectNodes("<#=childName#>")) 
             yield return new <#=UpperInitial(childName)#>(node); 
      } }
<#
         break;
         // Child attributes:
         case XmlNodeType.Attribute:
#>
      public string <#=childName #>
      { get { return thisNode.Attributes["<#=childName#>"].Value; } }
<#
         break;
         // Plain text:
         case XmlNodeType.Text:
#>
      public string Text  { get { return thisNode.InnerText; } }
<#
         break;
       } // switch
     } // foreach class child
  // End of the generated class:
#>
   } 
<#
 } // foreach class
 
   // Add a constructor for the root class 
   // that accepts an XML filename.
   string rootClassName = doc.SelectSingleNode("*").Name;
#>
   partial class <#= UpperInitial(rootClassName) #>
   {
      public <#= UpperInitial(rootClassName) #>(string fileName) 
      {
        XmlDocument doc = new XmlDocument();
        doc.Load(fileName);
        thisNode = doc.SelectSingleNode("<#=rootClassName#>");
      }
   }
}
<#+
   private string UpperInitial(string name)
   {
      return name[0].ToString().ToUpperInvariant() + name.Substring(1);
   }
#>

运行测试程

在控制台应用程序的主体中,以下行将执行测试方法。  按 F5 以调试模式运行该程序:  

using System;
namespace MyProject
{ class Program
  { static void Main(string[] args)
    { new CodeGeneratorTest().TestMethod();
      // Allow user to see the output:
      Console.ReadLine();
} } }

编写和更新应用程

现在,可以使用生成的类(而不是通用 XML 代码)以强类型样式编写应用程序。

在 XML 架构更改时,可以轻松生成新类。  编译器将告诉开发人员必须更新何处的应用程序代码。  

若要在示例 XML 文件更改时重新生成类,请在解决方案资源管理器工具栏中单击“转换所有模板”

结束

本演练演示了代码生成的多种方法和优势。

·         代码生成是指从模型创建应用程序的部分源代码。  模型以适合于应用程序域的形式包含信息,并且可以在应用程序的生存期更改。  

·         强类型是代码生成的一个优点。  模型以更适合于用户的形式表示信息,而生成的代码允许应用程序的其他部分使用一组类型处理该信息。  

·         在编写新代码和更新架构时,IntelliSense 和编译器可以帮助您创建遵循模型架构的代码。

·         将一个不复杂的模板文件添加到项目可以提供这些优势。

·         可以快速并以增量方式开发和测试文本模板。

在本演练中,程序代码实际上是从一个模型实例生成的,该模型是应用程序将处理的 XML 文件的一个代表性示例。  在更正式的方法中,XML 架构将以 .xsd 文件或域特定语言定义的形式输入到模板中。  该方法使模板更易于确定诸如关系的重数之类的特性。  

 

posted @ 2016-05-21 15:05  常想一二,不思八九  阅读(377)  评论(0)    收藏  举报