演练:创建自定义指令处理器
指令处理器通过向生成的转换类添加代码发挥作用 。 如果从文本模板调用指令,文本模板中编写的其余代码就可以利用该指令提供的功能 。
您可以编写自己的自定义指令处理器。 利用它可以自定义文本模板。 若要创建自定义指令处理器,需要创建一个从 DirectiveProcessor 或 RequiresProvidesDirectiveProcessor 继承的类。
本演练演示以下任务:
创建自定义指令处理器
注册指令处理器
测试指令处理器
创建自定义指令处理器
在本演练中,将创建一个自定义指令处理器。 添加一条自定义指令,该指令读取 XML 文件,将其存储在 XmlDocument 变量中,并通过一个属性将其公开。 在“测试指令处理器”一节中,将在文本模板中使用此属性访问 XML 文件。
自定义指令的调用如下所示:
<#@ CoolDirective Processor="CustomDirectiveProcessor" FileName="<Your Path>DocFile.xml" #>
自定义指令处理器将变量和属性添加到生成转换类。 您编写的指令使用 System.CodeDom 类创建引擎添加到生成转换类的代码。 System.CodeDom 类使用 Visual C# 或 Visual Basic 创建代码,具体取决于在 template
指令的 language
参数中指定的语言。 指令处理器的语言和访问指令处理器的文本模板的语言不必一致。
指令创建的代码如下所示:
private System.Xml.XmlDocument document0Value;
public virtual System.Xml.XmlDocument Document0
{
get
{
if ((this.document0Value == null))
{
this.document0Value = XmlReaderHelper.ReadXml(<FileNameParameterValue>);
}
return this.document0Value;
}
}
创建自定义指令处理器
在 Visual Studio 中,创建一个名为 CustomDP 的 C# 或 Visual Basic 类库项目。
注意
如果要在多台计算机上安装指令处理器,最好使用 Visual Studio Extension (VSIX) 项目并在扩展中包含一个 .pkgdef 文件。 有关详细信息,请参阅部署自定义指令处理器。
添加对下列程序集的引用:
Microsoft.VisualStudio.TextTemplating.*.0
Microsoft.VisualStudio.TextTemplating.Interfaces.*.0
用下面的代码替换“Class1”中的代码。 此代码定义一个继承自 DirectiveProcessor 类的 CustomDirectiveProcessor 类并实现必需的方法。
using System; using System.CodeDom; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using System.Xml; using System.Xml.Serialization; using Microsoft.VisualStudio.TextTemplating; namespace CustomDP { public class CustomDirectiveProcessor : DirectiveProcessor { // This buffer stores the code that is added to the // generated transformation class after all the processing is done. // --------------------------------------------------------------------- private StringBuilder codeBuffer; // Using a Code Dom Provider creates code for the // generated transformation class in either Visual Basic or C#. // If you want your directive processor to support only one language, you // can hard code the code you add to the generated transformation class. // In that case, you do not need this field. // -------------------------------------------------------------------------- private CodeDomProvider codeDomProvider; // This stores the full contents of the text template that is being processed. // -------------------------------------------------------------------------- private String templateContents; // These are the errors that occur during processing. The engine passes // the errors to the host, and the host can decide how to display them, // for example the host can display the errors in the UI // or write them to a file. // --------------------------------------------------------------------- private CompilerErrorCollection errorsValue; public new CompilerErrorCollection Errors { get { return errorsValue; } } // Each time this directive processor is called, it creates a new property. // We count how many times we are called, and append "n" to each new // property name. The property names are therefore unique. // ----------------------------------------------------------------------------- private int directiveCount = 0; public override void Initialize(ITextTemplatingEngineHost host) { // We do not need to do any initialization work. } public override void StartProcessingRun(CodeDomProvider languageProvider, String templateContents, CompilerErrorCollection errors) { // The engine has passed us the language of the text template // we will use that language to generate code later. // ---------------------------------------------------------- this.codeDomProvider = languageProvider; this.templateContents = templateContents; this.errorsValue = errors; this.codeBuffer = new StringBuilder(); } // Before calling the ProcessDirective method for a directive, the // engine calls this function to see whether the directive is supported. // Notice that one directive processor might support many directives. // --------------------------------------------------------------------- public override bool IsDirectiveSupported(string directiveName) { if (string.Compare(directiveName, "CoolDirective", StringComparison.OrdinalIgnoreCase) == 0) { return true; } if (string.Compare(directiveName, "SuperCoolDirective", StringComparison.OrdinalIgnoreCase) == 0) { return true; } return false; } public override void ProcessDirective(string directiveName, IDictionary<string, string> arguments) { if (string.Compare(directiveName, "CoolDirective", StringComparison.OrdinalIgnoreCase) == 0) { string fileName; if (!arguments.TryGetValue("FileName", out fileName)) { throw new DirectiveProcessorException("Required argument 'FileName' not specified."); } if (string.IsNullOrEmpty(fileName)) { throw new DirectiveProcessorException("Argument 'FileName' is null or empty."); } // Now we add code to the generated transformation class. // This directive supports either Visual Basic or C#, so we must use the // System.CodeDom to create the code. // If a directive supports only one language, you can hard code the code. // -------------------------------------------------------------------------- CodeMemberField documentField = new CodeMemberField(); documentField.Name = "document" + directiveCount + "Value"; documentField.Type = new CodeTypeReference(typeof(XmlDocument)); documentField.Attributes = MemberAttributes.Private; CodeMemberProperty documentProperty = new CodeMemberProperty(); documentProperty.Name = "Document" + directiveCount; documentProperty.Type = new CodeTypeReference(typeof(XmlDocument)); documentProperty.Attributes = MemberAttributes.Public; documentProperty.HasSet = false; documentProperty.HasGet = true; CodeExpression fieldName = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), documentField.Name); CodeExpression booleanTest = new CodeBinaryOperatorExpression(fieldName, CodeBinaryOperatorType.IdentityEquality, new CodePrimitiveExpression(null)); CodeExpression rightSide = new CodeMethodInvokeExpression(new CodeTypeReferenceExpression("XmlReaderHelper"), "ReadXml", new CodePrimitiveExpression(fileName)); CodeStatement[] thenSteps = new CodeStatement[] { new CodeAssignStatement(fieldName, rightSide) }; CodeConditionStatement ifThen = new CodeConditionStatement(booleanTest, thenSteps); documentProperty.GetStatements.Add(ifThen); CodeStatement s = new CodeMethodReturnStatement(fieldName); documentProperty.GetStatements.Add(s); CodeGeneratorOptions options = new CodeGeneratorOptions(); options.BlankLinesBetweenMembers = true; options.IndentString = " "; options.VerbatimOrder = true; options.BracingStyle = "C"; using (StringWriter writer = new StringWriter(codeBuffer, CultureInfo.InvariantCulture)) { codeDomProvider.GenerateCodeFromMember(documentField, writer, options); codeDomProvider.GenerateCodeFromMember(documentProperty, writer, options); } } // One directive processor can contain many directives. // If you want to support more directives, the code goes here... // ----------------------------------------------------------------- if (string.Compare(directiveName, "supercooldirective", StringComparison.OrdinalIgnoreCase) == 0) { // Code for SuperCoolDirective goes here... } // Track how many times the processor has been called. // ----------------------------------------------------------------- directiveCount++; } public override void FinishProcessingRun() { this.codeDomProvider = null; // Important: do not do this: // The get methods below are called after this method // and the get methods can access this field. // ----------------------------------------------------------------- // this.codeBuffer = null; } public override string GetPreInitializationCodeForProcessingRun() { // Use this method to add code to the start of the // Initialize() method of the generated transformation class. // We do not need any pre-initialization, so we will just return "". // ----------------------------------------------------------------- // GetPreInitializationCodeForProcessingRun runs before the // Initialize() method of the base class. // ----------------------------------------------------------------- return String.Empty; } public override string GetPostInitializationCodeForProcessingRun() { // Use this method to add code to the end of the // Initialize() method of the generated transformation class. // We do not need any post-initialization, so we will just return "". // ------------------------------------------------------------------ // GetPostInitializationCodeForProcessingRun runs after the // Initialize() method of the base class. // ----------------------------------------------------------------- return String.Empty; } public override string GetClassCodeForProcessingRun() { //Return the code to add to the generated transformation class. // ----------------------------------------------------------------- return codeBuffer.ToString(); } public override string[] GetReferencesForProcessingRun() { // This returns the references that we want to use when // compiling the generated transformation class. // ----------------------------------------------------------------- // We need a reference to this assembly to be able to call // XmlReaderHelper.ReadXml from the generated transformation class. // ----------------------------------------------------------------- return new string[] { "System.Xml", this.GetType().Assembly.Location }; } public override string[] GetImportsForProcessingRun() { //This returns the imports or using statements that we want to //add to the generated transformation class. // ----------------------------------------------------------------- //We need CustomDP to be able to call XmlReaderHelper.ReadXml //from the generated transformation class. // ----------------------------------------------------------------- return new string[] { "System.Xml", "CustomDP" }; } } // ------------------------------------------------------------------------- // The code that we are adding to the generated transformation class // will call this method. // ------------------------------------------------------------------------- public static class XmlReaderHelper { public static XmlDocument ReadXml(string fileName) { XmlDocument d = new XmlDocument(); using (XmlReader reader = XmlReader.Create(fileName)) { try { d.Load(reader); } catch (System.Xml.XmlException e) { throw new DirectiveProcessorException("Unable to read the XML file.", e); } } return d; } } }
仅对于 Visual Basic,打开“项目”菜单,单击“CustomDP 属性” 。 在“应用程序”选项卡上,在“根命名空间”中删除默认值
CustomDP
。在“文件” 菜单上,单击“全部保存” 。
在“生成”菜单中,单击“生成解决方案”。
生成项目
生成项目。 在“生成”菜单中,单击“生成解决方案”。
注册指令处理器
必须先为指令处理器添加注册表项,才能在 Visual Studio 中从文本模板调用指令。
注意
如果要在多台计算机上安装指令处理器,最好定义一个 Visual Studio Extension (VSIX),其中包含一个 .pkgdef 文件和你的程序集。 有关详细信息,请参阅部署自定义指令处理器。
指令处理器的项在注册表的以下位置:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors
对于 64 位系统,注册表位置为:
HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors
在本节中,将在注册表中的该位置为自定义指令处理器添加一个项。
注意
错误编辑注册表会严重损坏您的系统。 更改注册表之前,应备份计算机中的所有重要数据。
为指令处理器添加注册表项
使用“开始”菜单或命令行运行
regedit
命令。浏览到位置 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors,单击该节点。
在 64 位系统上,请使用 HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors
添加名为 CustomDirectiveProcessor 的新项。
注意
这是将在自定义指令的 Processor 字段中使用的名称。 此名称不必与指令名称、指令处理器类名称或指令处理器命名空间一致。
添加名为 Class 的新字符串值,该新字符串名称的值为 CustomDP.CustomDirectiveProcessor。
添加名为 CodeBase 的新字符串值,它的值等于在本演练前面创建的 CustomDP.dll 的路径。
例如,路径可能如下所示:
C:\UserFiles\CustomDP\bin\Debug\CustomDP.dll
。注册表项应具有以下值:
名称 类型 数据 (默认值) REG_SZ (未设置值) 类 REG_SZ CustomDP.CustomDirectiveProcessor CodeBase REG_SZ <解决方案的路径>CustomDP\bin\Debug\CustomDP.dll 如果已将程序集放置在 GAC 中,则值应如下所示:
名称 类型 数据 (默认值) REG_SZ (未设置值) 类 REG_SZ CustomDP.CustomDirectiveProcessor 程序集 REG_SZ CustomDP.dll 重新启动 Visual Studio。
测试指令处理器
若要测试指令处理器,需要编写一个调用它的文本模板。
在本示例中,文本模板调用指令并传入包含类文件文档的 XML 文件的名称。 然后,文本模板使用该指令创建的 XmlDocument 属性导航到 XML 并输出文档注释。
创建供测试指令处理器使用的 XML 文件
使用任意文本编辑器(如记事本)创建一个名为 DocFile.xml 的文件。
注意
可以在任意位置(如 C:\Test\DocFile.xml)创建此文件。
将以下代码添加到 XML 文件:
<?xml version="1.0"?> <doc> <assembly> <name>xmlsample</name> </assembly> <members> <member name="T:SomeClass"> <summary>Class level summary documentation goes here.</summary> <remarks>Longer comments can be associated with a type or member through the remarks tag</remarks> </member> <member name="F:SomeClass.m_Name"> <summary>Store for the name property</summary> </member> <member name="M:SomeClass.#ctor"> <summary>The class constructor.</summary> </member> <member name="M:SomeClass.SomeMethod(System.String)"> <summary>Description for SomeMethod.</summary> <param name="s">Parameter description for s goes here</param> <seealso cref="T:System.String">You can use the cref attribute on any tag to reference a type or member and the compiler will check that the reference exists.</seealso> </member> <member name="M:SomeClass.SomeOtherMethod"> <summary>Some other method.</summary> <returns>Return results are described through the returns tag.</returns> <seealso cref="M:SomeClass.SomeMethod(System.String)">Notice the use of the cref attribute to reference a specific method</seealso> </member> <member name="M:SomeClass.Main(System.String[])"> <summary>The entry point for the application.</summary> <param name="args">A list of command line arguments</param> </member> <member name="P:SomeClass.Name"> <summary>Name property</summary> <value>A value tag is used to describe the property value</value> </member> </members> </doc>
保存并关闭该文件。
创建文本模板测试指令处理器
在 Visual Studio 中,创建一个名为 TemplateTest 的 C# 或 Visual Basic 类库项目。
添加名为 TestDP.tt 的新文本模板文件。
确保将 TestDP.tt 的“自定义工具”属性设置为
TextTemplatingFileGenerator
。将 TestDP.tt 的内容更改为以下文本。
注意
将字符串
<YOUR PATH>
替换为 DocFile.xml 文件的路径。文本模板的语言不必与指令处理器的语言一致。
<#@ assembly name="System.Xml" #> <#@ template debug="true" #> <#@ output extension=".txt" #> <# // This will call the custom directive processor. #> <#@ CoolDirective Processor="CustomDirectiveProcessor" FileName="<YOUR PATH>\DocFile.xml" #> <# // Uncomment this line if you want to see the generated transformation class. #> <# // System.Diagnostics.Debugger.Break(); #> <# // This will use the results of the directive processor. #> <# // The directive processor has read the XML and stored it in Document0. #> <# XmlNode node = Document0.DocumentElement.SelectSingleNode("members"); foreach (XmlNode member in node.ChildNodes) { XmlNode name = member.Attributes.GetNamedItem("name"); WriteLine("{0,7}: {1}", "Name", name.Value); foreach (XmlNode comment in member.ChildNodes) { WriteLine("{0,7}: {1}", comment.Name, comment.InnerText); } WriteLine(""); } #> <# // You can call the directive processor again and pass it a different file. #> <# // @ CoolDirective Processor="CustomDirectiveProcessor" FileName="<YOUR PATH>\<Your Second File>" #> <# // To use the results of the second directive call, use Document1. #> <# // XmlNode node2 = Document1.DocumentElement.SelectSingleNode("members"); // ... #>
注意
在本示例中,
Processor
参数的值为CustomDirectiveProcessor
。Processor
参数的值必须与处理器的注册表项的名称一致。在“文件”菜单中,选择“全部保存” 。
测试指令处理器
在“解决方案资源管理器”中,右击 TestDP.tt,然后单击“运行自定义工具” 。
对于 Visual Basic 用户,默认情况下,TestDP.txt 可能不会显示在“解决方案资源管理器”中。 若要显示分配给项目的所有文件,请打开“项目”菜单并单击“显示所有文件” 。
在“解决方案资源管理器”中,展开 TestDP.txt 节点,然后双击 TestDP.txt,在编辑器中将其打开。
此时将显示生成的文本输出。 输出应如下所示:
Name: T:SomeClass summary: Class level summary documentation goes here. remarks: Longer comments can be associated with a type or member through the remarks tag Name: F:SomeClass.m_Name summary: Store for the name property Name: M:SomeClass.#ctor summary: The class constructor. Name: M:SomeClass.SomeMethod(System.String) summary: Description for SomeMethod. param: Parameter description for s goes here seealso: You can use the cref attribute on any tag to reference a type or member and the compiler will check that the reference exists. Name: M:SomeClass.SomeOtherMethod summary: Some other method. returns: Return results are described through the returns tag. seealso: Notice the use of the cref attribute to reference a specific method Name: M:SomeClass.Main(System.String[]) summary: The entry point for the application. param: A list of command line arguments Name: P:SomeClass.Name summary: Name property value: A value tag is used to describe the property value
向生成的文本添加 HTML
测试自定义指令处理器后,可能要向生成的文本添加一些 HTML。
向生成的文本添加 HTML
用下面的代码替换 TestDP.tt 中的代码。 HTML 为突出显示状态。 确保将字符串
YOUR PATH
替换为 DocFile.xml 文件的路径。注意
附加的开始 <# 和结束 #> 标记将语句代码与 HTML 标记区分开来。
<#@ assembly name="System.Xml" #> <#@ template debug="true" #> <#@ output extension=".htm" #> <# // This will call the custom directive processor #> <#@ CoolDirective Processor="CustomDirectiveProcessor" FileName="<YOUR PATH>\DocFile.xml" #> <# // Uncomment this line if you want to see the generated transformation class #> <# // System.Diagnostics.Debugger.Break(); #> <html><body> <# // This will use the results of the directive processor #>. <# // The directive processor has read the XML and stored it in Document0#>. <# XmlNode node = Document0.DocumentElement.SelectSingleNode("members"); foreach (XmlNode member in node.ChildNodes) { #> <h3> <# XmlNode name = member.Attributes.GetNamedItem("name"); WriteLine("{0,7}: {1}", "Name", name.Value); #> </h3> <# foreach (XmlNode comment in member.ChildNodes) { WriteLine("{0,7}: {1}", comment.Name, comment.InnerText); #> <br/> <# } } #> </body></html>
在“文件”菜单上,单击“保存 TestDP.txt” 。
若要在浏览器中查看输出,请在“解决方案资源管理器”中右击 TestDP.htm,再单击“在浏览器中查看” 。
输出应与原始文本相同,只是它应用了 HTML 格式。 每个项名称都以粗体显示。