Поделиться через


Пошаговое руководство. Создание пользовательского обработчика директив

Процессоры директив работают путем добавления кода в созданный класс преобразования. При вызове директивы из текстового шаблона остальная часть кода, написанного в текстовом шаблоне, может полагаться на функциональность, которую предоставляет директива.

Можно написать собственные пользовательские процессоры директив. Это позволяет настраивать текстовые шаблоны. Для создания пользовательского процессора директив создается класс, наследующий DirectiveProcessor либо RequiresProvidesDirectiveProcessor.

В данном пошаговом руководстве иллюстрируется выполнение следующих задач.

  • Создание пользовательского обработчика директив

  • Регистрация обработчика директив

  • Тестирование обработчика директив

Создание пользовательского обработчика директив

В данном пошаговом руководстве вы создадите пользовательский процессор директив. Вы добавите пользовательскую директиву, которая считывает XML-файл, сохраняет его в переменной XmlDocument и предоставляет доступ к нему через свойство. В разделе "Тестирование процессора директив" вы используете это свойство в текстовом шаблоне для доступа к XML-файлу.

Вызов пользовательской директивы выглядит так:

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

Пользовательский процессор директив добавляет переменную и свойство к сгенерированному классу преобразования. Директива, которую вы пишете, использует классы System.CodeDom для создания кода, который процессор добавляет в сгенерированный класс преобразования. Классы System.CodeDom создают код в Visual C# или Visual Basic в зависимости от языка, указанного в language параметре template директивы. Язык процессора директив и язык текстового шаблона, осуществляющего доступ к этому процессору, не обязательно должен совпадать.

Создаваемый этой директивой код выглядит так:

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 создайте проект библиотеки классов C# или Visual Basic с именем CustomDP.

    Примечание.

    Если вы хотите установить обработчик директив на нескольких компьютерах, лучше использовать проект расширения Visual Studio (VSIX) и включить pkgdef-файл в расширение. Дополнительные сведения см. в разделе "Развертывание пользовательского обработчика директив".

  2. Добавьте ссылки на следующие сборки:

    • Microsoft.VisualStudio.TextTemplating.*.0

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

  3. Замените код в классе1 следующим кодом. Этот код определяет класс CustomDirectiveProcessor, наследующий класс DirectiveProcessor и реализующий необходимые методы.

    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. В меню File (Файл) выберите команду Save All (Сохранить все).

  6. В меню Сборка выберите Построить решение.

Постройте проект.

Выполните сборку проекта. В меню Сборка выберите Построить решение.

Регистрация обработчика директив

Прежде чем вызывать директиву из текстового шаблона в Visual Studio, необходимо добавить раздел реестра для обработчика директив.

Примечание.

Если вы хотите установить процессор директив на нескольких компьютерах, лучше определить расширение Visual Studio (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образом.

    Ваш раздел реестра должен содержать следующие значения:

    Имя. Тип Data
    (по умолчанию) REG_SZ (значение не задано)
    Класс REG_SZ CustomDP.CustomDirectiveProcessor
    CodeBase REG_SZ <Путь к вашему решению>CustomDP\bin\Debug\CustomDP.dll

    Если вы поместили сборку в глобальный кэш сборок, эти значения должны выглядеть так:

    Имя. Тип Data
    (по умолчанию) 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 создайте проект библиотеки классов C# или Visual Basic с именем TemplateTest.

  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 является CustomDirectiveProcessor. Значение параметра Processor должно соответствовать имени раздела реестра процессора.

  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. Каждое имя элемента отображается полужирным шрифтом.