演练:创建自定义指令处理器

指令处理器通过向生成的转换类添加代码发挥作用 。 如果从文本模板调用指令,文本模板中编写的其余代码就可以利用该指令提供的功能 。

您可以编写自己的自定义指令处理器。 利用它可以自定义文本模板。 若要创建自定义指令处理器,需要创建一个从 DirectiveProcessorRequiresProvidesDirectiveProcessor 继承的类。

本演练演示以下任务:

  • 创建自定义指令处理器

  • 注册指令处理器

  • 测试指令处理器

创建自定义指令处理器

在本演练中,将创建一个自定义指令处理器。 添加一条自定义指令,该指令读取 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;
  }
}

创建自定义指令处理器

  1. 在 Visual Studio 中,创建一个名为 CustomDP 的 C# 或 Visual Basic 类库项目。

    注意

    如果要在多台计算机上安装指令处理器,最好使用 Visual Studio Extension (VSIX) 项目并在扩展中包含一个 .pkgdef 文件。 有关详细信息,请参阅部署自定义指令处理器

  2. 添加对下列程序集的引用:

    • Microsoft.VisualStudio.TextTemplating.*.0

    • Microsoft.VisualStudio.TextTemplating.Interfaces.*.0

  3. 用下面的代码替换“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;
            }
        }
    }
    
  4. 仅对于 Visual Basic,打开“项目”菜单,单击“CustomDP 属性” 。 在“应用程序”选项卡上,在“根命名空间”中删除默认值 CustomDP

  5. 在“文件” 菜单上,单击“全部保存” 。

  6. 在“生成”菜单中,单击“生成解决方案”。

生成项目

生成项目。 在“生成”菜单中,单击“生成解决方案”。

注册指令处理器

必须先为指令处理器添加注册表项,才能在 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

在本节中,将在注册表中的该位置为自定义指令处理器添加一个项。

注意

错误编辑注册表会严重损坏您的系统。 更改注册表之前,应备份计算机中的所有重要数据。

为指令处理器添加注册表项

  1. 使用“开始”菜单或命令行运行 regedit 命令。

  2. 浏览到位置 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors,单击该节点。

    在 64 位系统上,请使用 HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors

  3. 添加名为 CustomDirectiveProcessor 的新项。

    注意

    这是将在自定义指令的 Processor 字段中使用的名称。 此名称不必与指令名称、指令处理器类名称或指令处理器命名空间一致。

  4. 添加名为 Class 的新字符串值,该新字符串名称的值为 CustomDP.CustomDirectiveProcessor。

  5. 添加名为 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
  6. 重新启动 Visual Studio。

测试指令处理器

若要测试指令处理器,需要编写一个调用它的文本模板。

在本示例中,文本模板调用指令并传入包含类文件文档的 XML 文件的名称。 然后,文本模板使用该指令创建的 XmlDocument 属性导航到 XML 并输出文档注释。

创建供测试指令处理器使用的 XML 文件

  1. 使用任意文本编辑器(如记事本)创建一个名为 DocFile.xml 的文件。

    注意

    可以在任意位置(如 C:\Test\DocFile.xml)创建此文件。

  2. 将以下代码添加到 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>
    
  3. 保存并关闭该文件。

创建文本模板测试指令处理器

  1. 在 Visual Studio 中,创建一个名为 TemplateTest 的 C# 或 Visual Basic 类库项目。

  2. 添加名为 TestDP.tt 的新文本模板文件。

  3. 确保将 TestDP.tt 的“自定义工具”属性设置为 TextTemplatingFileGenerator

  4. 将 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 参数的值为 CustomDirectiveProcessorProcessor 参数的值必须与处理器的注册表项的名称一致。

  5. 在“文件”菜单中,选择“全部保存” 。

测试指令处理器

  1. 在“解决方案资源管理器”中,右击 TestDP.tt,然后单击“运行自定义工具” 。

    对于 Visual Basic 用户,默认情况下,TestDP.txt 可能不会显示在“解决方案资源管理器”中。 若要显示分配给项目的所有文件,请打开“项目”菜单并单击“显示所有文件” 。

  2. 在“解决方案资源管理器”中,展开 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

  1. 用下面的代码替换 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>
    
  2. 在“文件”菜单上,单击“保存 TestDP.txt” 。

  3. 若要在浏览器中查看输出,请在“解决方案资源管理器”中右击 TestDP.htm,再单击“在浏览器中查看” 。

    输出应与原始文本相同,只是它应用了 HTML 格式。 每个项名称都以粗体显示。