演练:使用文本模板生成代码
通过代码生成,可以生成在源模型更改时能够轻松更改的强类型程序代码。 将此方法与编写完全通用程序(接受配置文件)的备选方法对比,可以发现后者虽然更灵活,但导致代码既没有这么容易读取和更改,也没有这么好的性能。 本演练演示了此优势。
用于读取 XML 的指定类型化代码
System.Xml 命名空间提供全面的工具,用于加载 XML 文档,然后在内存中自由导航该文档。 不过,所有节点都具有相同的类型 XmlNode。 因此,很容易造成编程错误,例如出现错误类型的子节点或错误特性。
在此示例项目中,模板读取示例 XML 文件,然后生成与每种节点类型对应的类。 在手写代码中,可以使用这些类导航 XML 文件。 还可以针对使用相同节点类型的其他任何文件运行应用程序。 此示例 XML 文件的作用是提供您希望应用程序处理的所有节点类型的示例。
备注
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# 项目,并且出于测试目的,我们使用了一个控制台应用程序。
创建项目
在**“文件”菜单上,单击“新建”,然后单击“项目”**。
单击**“Visual C#”节点,然后在“模板”窗格中,单击“控制台应用程序”**。
将原型 XML 文件添加到项目
此文件的作用是提供您希望应用程序能够读取的 XML 节点类型的示例。 它可以是一个将用于测试应用程序的文件。 模板将为此文件中的每个节点类型生成一个 C# 类。
此文件应是项目的一部分以便模板能够读取,但不会内置到编译的应用程序中。
添加 XML 文件
在**“解决方案资源管理器”中右击项目,单击“添加”,然后单击“新建项”**。
在**“添加新项目”对话框的“模板”窗格中,选择“XML 文件”**。
将示例内容添加到文件中。
对于本演练,将文件命名为 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”。
将文本模板文件添加到项目
在**“解决方案资源管理器”中右击项目,单击“添加”,然后单击“新建项”**。
在**“添加新项目”对话框的“模板”窗格中,选择“文本模板”**。
备注
确保添加的是文本模板,而不是预处理文本模板。
在该文件的模板指令中,将 hostspecific 特性更改为 true。
此更改将使模板代码能够获取对 Visual Studio 服务的访问。
在输出指令中,将扩展特性更改为“.cs”,以便模板生成一个 C# 文件。 在 Visual Basic 项目中,应将其更改为“.vb”。
保存该文件。 在此阶段,该文本模板文件应包含以下行:
<#@ template debug="false" hostspecific="true" language="C#" #> <#@ 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 文件或域特定语言定义的形式输入到模板中。 该方法使模板更易于确定诸如关系的重数之类的特性。
排除文本模板的故障
如果您已在**“错误列表”**中看到模板转换或编译错误,或者未正确生成输出文件,则可以使用使用 TextTransform 实用工具生成文件中描述的方法排除文本模板的故障。