Share via


Tutorial: Crear un procesador de directivas personalizadas

Los procesadores de directivas agregan código a la clase de transformación generada. Si llama a una directiva desde una plantilla de texto, el resto del código que escribe en la plantilla de texto puede basarse en la funcionalidad que la directiva proporciona.

Puede escribir sus propios procesadores de directivas personalizados. Esto le permite personalizar las plantillas de texto. Para crear un procesador de directivas personalizado, crea una clase que herede de DirectiveProcessor o RequiresProvidesDirectiveProcessor.

Las tareas que se ilustran en este tutorial son las siguientes:

  • Creación de un procesador de directivas personalizado

  • Registro del procesador de directivas

  • Prueba del procesador de directivas

Creación de un procesador de directivas personalizado

En este tutorial, creará un procesador de directivas personalizado. Agregará una directiva personalizada que lea un archivo XML, le almacene en una variable XmlDocument y lo exponga a través de una propiedad. En la sección "Probar el procesador de directivas", utilizará esta propiedad en una plantilla de texto para tener acceso al archivo XML.

La llamada a la directiva personalizada tiene un aspecto similar al siguiente:

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

El procesador de directivas personalizado agrega la variable y la propiedad a la clase de transformación generada. La directiva que escribe utiliza las clases System.CodeDom para crear el código que el motor agrega a la clase de transformación generada. Las clases System.CodeDom crean código en Visual C# o Visual Basic, dependiendo del lenguaje especificado en el parámetro language de la directiva template. El lenguaje del procesador de directivas y el lenguaje de la plantilla de texto que tiene acceso al procesador de directivas no tienen que coincidir.

El código que crea la directiva tiene un aspecto similar al siguiente:

private System.Xml.XmlDocument document0Value;

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

Para crear un procesador de directivas personalizado

  1. En Visual Studio, cree un proyecto de biblioteca de clases de Visual C# o Visual Basic con el nombre CustomDP.

    Nota

    Si desea instalar el procesador de directivas en más de un equipo, es mejor utilizar un proyecto de extensión de Visual Studio (VSIX) e incluir un archivo .pkgdef en la extensión. Para más información, consulte Implementación de un procesador de directivas personalizado.

  2. Agregue referencias a estos ensamblados:

    • Microsoft.VisualStudio.TextTemplating.*.0

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

  3. Reemplace el código de Class1 por el siguiente código. Este código define una clase CustomDirectiveProcessor que hereda de la clase DirectiveProcessor e implementa los métodos necesarios.

    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. Solo para Visual Basic, abra el menú Proyecto y haga clic en Propiedades de CustomDP. En la pestaña Aplicación, en Espacio de nombres raíz, elimine el valor predeterminado, CustomDP.

  5. En el menú Archivo , haga clic en Guardar todo.

  6. En el menú Compilar, haga clic en Compilar solución.

Compilar el proyecto

Compile el proyecto. En el menú Compilar , haga clic en Compilar solución.

Registro del procesador de directivas

Antes de llamar a una directiva desde una plantilla de texto en Visual Studio, debe agregar una clave del Registro para el procesador de directivas.

Nota

Si desea instalar el procesador de directivas en más de un equipo, es mejor definir una extensión de Visual Studio (VSIX) que incluya un archivo .pkgdef junto con el ensamblado. Para más información, consulte Implementación de un procesador de directivas personalizado.

Las claves para los procesadores de directivas se encuentran en la siguiente ubicación del Registro:

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

Para los sistemas 64 bits, la ubicación del Registro es:

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

En esta sección, agregará una clave para el procesador de directivas personalizado en el Registro, en la misma ubicación.

Precaución

Una modificación incorrecta del Registro puede provocar daños graves en el sistema. Antes de efectuar cambios en el Registro, realice una copia de seguridad de la información importante del equipo.

Para agregar una clave del Registro para el procesador de directivas

  1. Ejecute el comando regedit desde el menú Inicio o la línea de comandos.

  2. Desplácese a la ubicación HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio \*.0\TextTemplating\DirectiveProcessors y haga clic en el nodo.

    En los sistemas 64 bits, utilice HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\*.0\TextTemplating\DirectiveProcessors

  3. Agregue una nueva clave con el nombre CustomDirectiveProcessor.

    Nota

    Este es el nombre que utilizará en el campo de procesador de las directivas personalizadas. Este nombre no tiene que coincidir con el nombre de la directiva, el nombre de la clase de procesador de directivas ni el espacio de nombres de procesador de directivas.

  4. Agregue un nuevo valor de cadena denominado Class con un valor CustomDP.CustomDirectiveProcessor para el nombre de la nueva cadena.

  5. Agregue un nuevo valor de cadena denominado CodeBase con un valor igual a la ruta de acceso de CustomDP.dll que creó anteriormente en este tutorial.

    Por ejemplo, la ruta de acceso podría ser similar a C:\UserFiles\CustomDP\bin\Debug\CustomDP.dll.

    La clave del Registro debe tener los siguientes valores:

    Nombre Tipo Datos
    (Es el valor predeterminado). REG_SZ (valor no establecido)
    Clase REG_SZ CustomDP.CustomDirectiveProcessor
    CodeBase REG_SZ <Ruta de acceso a la solución>CustomDP\bin\Debug\CustomDP.dll

    Si ha colocado el ensamblado en la GAC, los valores deberían ser similares a los siguientes:

    Nombre Tipo Datos
    (Es el valor predeterminado). REG_SZ (valor no establecido)
    Clase REG_SZ CustomDP.CustomDirectiveProcessor
    Ensamblado REG_SZ CustomDP.dll
  6. Reinicie Visual Studio.

Prueba del procesador de directivas

Para probar el procesador de directivas, debe escribir una plantilla de texto que lo llame.

En este ejemplo, la plantilla de texto llama a la directiva y le pasa en el nombre de un archivo XML que contiene documentación para un archivo de clase. La plantilla de texto utiliza la propiedad XmlDocument que la directiva crea para navegar al XML e imprimir los comentarios de la documentación.

Para crear un archivo XML y usarlo para probar el procesador de directivas

  1. Cree un archivo denominado DocFile.xml desde cualquier editor de texto (por ejemplo, Bloc de notas).

    Nota

    Puede crear este archivo en cualquier ubicación (por ejemplo, C:\Test\DocFile.xml).

  2. Agregue lo siguiente al archivo 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. Guarde y cierre el archivo.

Para crear una plantilla de texto y probar el procesador de directivas

  1. En Visual Studio, cree un proyecto de biblioteca de clases de Visual C# o Visual Basic con el nombre TemplateTest.

  2. Agregue un nuevo archivo de plantilla de texto denominado TestDP.tt.

  3. Asegúrese de que la propiedad Custom Tool de TestDP.tt esté establecida en TextTemplatingFileGenerator.

  4. Cambie el contenido de TestDP.tt al siguiente texto.

    Nota:

    Reemplace la cadena <YOUR PATH> por la ruta de acceso al archivo DocFile.xml.

    El lenguaje de la plantilla de texto no tiene que coincidir con el lenguaje del procesador de directivas.

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

    Nota

    En este ejemplo, el valor del parámetro Processor es CustomDirectiveProcessor. El valor del parámetro Processor debe coincidir con el nombre de la clave del Registro del procesador.

  5. En el menú Archivo, elija Guardar todo.

Para probar el procesador de directivas

  1. En el Explorador de soluciones, haga clic con el botón secundario en TestDP.tt y, después, haga clic en Ejecutar herramienta personalizada.

    En el caso de los usuarios de Visual Basic, es posible que TestDP.txt no aparezca en el Explorador de soluciones de forma predeterminada. Para mostrar todos los archivos asignados al proyecto, abra el menú Proyecto y haga clic en Mostrar todos los archivos.

  2. En el Explorador de soluciones, expanda el nodo TestDP.txt y, después, haga doble clic en TestDP.txt para abrirlo en el editor.

    Aparece la salida de texto generada. La salida debe tener un aspecto similar al siguiente:

       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
    

Adición de HTML al texto generado

Después de probar el procesador de directivas personalizado, es posible que desee agregar algún HTML al texto generado.

Para agregar HTML al texto generado

  1. Reemplace el código de TestDP.tt por el siguiente. El HTML se resalta. Asegúrese de que reemplaza la cadena YOUR PATH por la ruta de acceso al archivo DocFile.xml.

    Nota:

    Las etiquetas de apertura <# y cierre #> adicionales separan el código de instrucción de las etiquetas 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. En el menú Archivo, haga clic en Guardar TestDP.txt.

  3. Para ver el resultado en un explorador, en el Explorador de soluciones, haga clic con el botón derecho en TestDP.htm y haga clic en Ver en el explorador.

    El resultado debe ser el mismo que el texto original, pero con el formato HTML aplicado. Todos los nombres de elemento aparecen en negrita.