Edit

Share via


Walkthrough: Create a Custom Directive Processor

Directive processors work by adding code to the generated transformation class. If you call a directive from a text template, the rest of the code that you write in your text template can rely on the functionality that the directive provides.

You can write your own custom directive processors. This enables you to customize your text templates. To create a custom directive processor, you create a class that inherits from either DirectiveProcessor or RequiresProvidesDirectiveProcessor.

Tasks that are illustrated in this walkthrough include the following:

  • Create a custom directive processor

  • Register the directive processor

  • Test the directive processor

Create a Custom Directive Processor

In this walkthrough, you create a custom directive processor. You add a custom directive that reads an XML file, stores it in an XmlDocument variable, and exposes it through a property. In the section "Testing the Directive Processor," you use this property in a text template to access the XML file.

The call to your custom directive looks like the following:

<#@ CoolDirective Processor="CustomDirectiveProcessor" FileName="<Your Path>DocFile.xml" #>

The custom directive processor adds the variable and the property to the generated transformation class. The directive that you write uses the System.CodeDom classes to create the code that the engine adds to the generated transformation class. The System.CodeDom classes create code in either Visual C# or Visual Basic, depending on the language specified in the language parameter of the template directive. The language of the directive processor and the language of the text template that is accessing the directive processor do not have to match.

The code that the directive creates looks like the following:

private System.Xml.XmlDocument document0Value;

public virtual System.Xml.XmlDocument Document0
{
  get
  {
    if ((this.document0Value == null))
    {
      this.document0Value = XmlReaderHelper.ReadXml(<FileNameParameterValue>);
    }
    return this.document0Value;
  }
}

To create a custom directive processor

  1. In Visual Studio, create a C# or a Visual Basic class library project named CustomDP.

    Note

    If you want to install the directive processor on more than one computer, it is better to use a Visual Studio Extension (VSIX) project and include a .pkgdef file in the extension. For more information, see Deploying a Custom Directive Processor.

  2. Add references to these assemblies:

    • Microsoft.VisualStudio.TextTemplating.*.0

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

  3. Replace the code in Class1 with the following code. This code defines a CustomDirectiveProcessor class that inherits from the DirectiveProcessor class and implements the necessary methods.

    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. For Visual Basic only, open the Project menu, and click CustomDP Properties. On the Application tab, in Root namespace, delete the default value, CustomDP.

  5. On the File menu, click Save All.

  6. On the Build menu, click Build Solution.

Build the Project

Build the project. On the Build menu, click Build Solution.

Register the Directive Processor

Before you can call a directive from a text template in Visual Studio, you must add a registry key for the directive processor.

Note

If you want to install the directive processor on more than one computer, it is better to define a Visual Studio Extension (VSIX) that includes a .pkgdef file along with your assembly. For more information, see Deploying a Custom Directive Processor.

Keys for directive processors exist in the registry in the following location:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors

For 64-bit systems, the registry location is:

HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors

In this section, you add a key for your custom directive processor to the registry in the same location.

Caution

Incorrectly editing the registry can severely damage your system. Before you make changes to the registry, back up any valuable data that is on the computer.

To add a registry key for the directive processor

  1. Run the regedit command by using the Start menu or the command line.

  2. Browse to the location HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors, and click the node.

    On 64-bit systems, use HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors

  3. Add a new key named CustomDirectiveProcessor.

    Note

    This is the name that you will use in the Processor field of your custom directives. This name does not need to match the name of the directive, the name of the directive processor class, or the directive processor namespace.

  4. Add a new string value named Class that has a value CustomDP.CustomDirectiveProcessor for the name of the new string.

  5. Add a new string value named CodeBase that has a value equal to the path of the CustomDP.dll that you created earlier in this walkthrough.

    For example, the path might look like C:\UserFiles\CustomDP\bin\Debug\CustomDP.dll.

    Your registry key should have the following values:

    Name Type Data
    (Default) REG_SZ (value not set)
    Class REG_SZ CustomDP.CustomDirectiveProcessor
    CodeBase REG_SZ <Path to Your Solution>CustomDP\bin\Debug\CustomDP.dll

    If you have put the assembly in the GAC, the values should look like the following:

    Name Type Data
    (Default) REG_SZ (value not set)
    Class REG_SZ CustomDP.CustomDirectiveProcessor
    Assembly REG_SZ CustomDP.dll
  6. Restart Visual Studio.

Test the Directive Processor

To test the directive processor, you need to write a text template that calls it.

In this example, the text template calls the directive and passes in the name of an XML file that contains documentation for a class file. The text template uses the XmlDocument property that the directive creates to navigate the XML and print the documentation comments.

To create an XML file for use in testing the directive processor

  1. Create a file named DocFile.xml by using any text editor (for example, Notepad).

    Note

    You can create this file in any location (for example, C:\Test\DocFile.xml).

  2. Add the following to the XML file:

    <?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. Save and close the file.

To create a text template to test the directive processor

  1. In Visual Studio, create a C# or Visual Basic class library project named TemplateTest.

  2. Add a new text template file named TestDP.tt.

  3. Make sure that the Custom Tool property of TestDP.tt is set to TextTemplatingFileGenerator.

  4. Change the content of TestDP.tt to the following text.

    Note

    Replace the string <YOUR PATH> with the path to the DocFile.xml file.

    The language of the text template does not have to match the language of the directive processor.

    <#@ 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");
    
        // ...
    #>
    

    Note

    In this example, the value of the Processor parameter is CustomDirectiveProcessor. The value of the Processor parameter must match the name of the processor's registry key.

  5. On the File menu, choose Save All.

To test the directive processor

  1. In Solution Explorer, right-click TestDP.tt and then click Run Custom Tool.

    For Visual Basic users, TestDP.txt might not appear in Solution Explorer by default. To display all files assigned to the project, open the Project menu and click Show All Files.

  2. In Solution Explorer, expand the TestDP.txt node, and then double-click TestDP.txt to open it in the editor.

    The generated text output appears. The output should look like the following:

       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
    

Add HTML to Generated Text

After you test your custom directive processor, you might want to add some HTML to your generated text.

To add HTML to the generated text

  1. Replace the code in TestDP.tt with the following. The HTML is highlighted. Make sure to replace the string YOUR PATH with the path to the DocFile.xml file.

    Note

    Additional open <# and close #> tags separate the statement code from the HTML tags.

    <#@ 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. On the File menu, click Save TestDP.txt.

  3. To view the output in a browser, in Solution Explorer, right-click TestDP.htm, and click View In Browser.

    Your output should be the same as the original text, except it has the HTML format applied. Each item name appears in bold.