Введение в генерацию программного кода

Технология текстовых шаблонов T4

Показывать лекцию целиком

Введение

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

Наглядным и удобным способом генерации кода является применение текстовых шаблонов. Техники применения шаблонов являются в большей степени декларативными способами генерации текста. Шаблон уже предоставляет макет кода в естественном для программиста виде. Неизменяемая часть кода уже заложена в шаблоне в том же самом виде, что и готовый сгенерированный код. С помощью специальных команд, инструкций, выполнения программного кода выводятся те части шаблона, которые не являются статическими, а меняются в зависимости от метаданных и настроек. Текстовые шаблоны позволяют упрощать генерацию кода. Если нужно изменить создаваемый код, то не потребуется разбираться в программе, генерирующей код, а достаточно изменить наглядный шаблон. Однако шаблоны эффективно применимы только тогда, когда повторяющийся код встречается часто во многих участках приложения, и его объем достаточно большой.

Технология T4 (Text Templating Transformation Toolkit) является инструментом генерации кода на основе шаблонов. Она включена в Visual Studio 2008 и Visual Studio 2010. А в Visual Studio 2005 она доступна при установке DSL (Domain-Specific Language) и GAT(Guidance Automation Toolkit). С помощью текстовых шаблонов можно сгенерировать все виды текстовой информации, так называемых артефактов. Это может быть программный код на C#, VB.Net, разметка HTML, запросы SQL, документация и любая другая текстовая информация. Технологию T4 можно применять как для генерации отдельных файлов, так и крупных приложений.

Создание файла шаблона

Для создания простого шаблона T4 надо выполнить следующие шаги:

  • Создать проект в Visual Studio.
  • Выбрать добавление нового пункта(файла) в проект.
  • Для Visual Studio 2010 выбрать Text Template. Для Visual Studio 2008 и 2005 выбрать Text File и поменять расширение файла на ".tt".
  • Дать название файлу (например, MyClass.tt) и добавить его в проект.
  • Добавить текст шаблона в файл. Например, такой:
    <#@ template debug="false" language="C#" #>
    <#@ output extension=".cs" #>
    <# string[] vars = new string [] {"A", "B", "C"}; #>
    class MyClass {
    <# 
      foreach (string variable in vars)
      { #>
      private int <#= variable #> = 0;
    <# } #>
    }
          
  • Сохранить файл.
  • После этого шага под файлом шаблона появится файл кода на C#, как на следующем рисунке.

    (рис 4.1) Файл кода, сгенерированного на основе шаблона T4

    Мы уже рассматривали этот пример во второй лекции. Если посмотрим на код сгенерированного файла MyClass.cs, то он будет таким.

    class MyClass {
      private int A = 0; 
      private int B = 0;
      private int C = 0;
    }
        

    Синтаксис T4

    Шаблонные файлы имеют синтаксис, похожий на страницы ASP.Net. Встроенный редактор T4 не выполняет даже простейшую подсветку синтаксиса. Также не работает IntelliSense. То есть редактор T4 в Visual Studio больше напоминает простой текстовый редактор. Однако есть продукты сторонних производителей с большими возможностями, которые в основном хоть и платные, но имеют урезанные бесплатные версии. Например, есть редактор Т4 от Tangible Engineering.

    В шаблонах T4 применяются следующие виды блоков: <#@ #> - для директив, <# #> - для блоков кода, <#+ #> - для блоков вспомогательных методов, <#= #> - блоков выражений. Все, что не входит в вышеперечисленные блоки, считается блоками текста. Они не обрамляются никакими тегами. Что мы видим в блоках текста, то будет и отображено без изменений в сгенерированном коде.

    Директивы являются элементами, которые указывают, как именно должен обрабатываться шаблон. Управляющими блоками считаются блоки кода, вспомогательных методов, а также блоки выражений. Именно управляющие блоки являются программным кодом, который манипулирует участками текста и осуществляет вывод в результат работы шаблона.

    Блоки кода

    Блоки кода вида <#...#> содержат код на C# или Visual Basic. В ходе своей работы этот код может выводить текст в файл результата шаблона.

    public class MyClass
    {
    <#
    for (int i=0;i < 2;i++)
    {
    #>
      public void DoAction()
      {
      }
    <#
    }
    #>
    }
            

    Результатом работы вышеуказанного шаблона будет следующий код:

    public class MyClass
    {
      public void DoAction()
      {
      }
      public void DoAction()
      {
      }
    }
            

    Этот сгенерированный код некорректен, так как в классе содержатся два метода с одним именем и сигнатурой. Но сейчас это не имеет большого значения, важен сам факт вывода кода.

    Блоки выражений

    Блоки выражений <#=...#> служат для записи результатов выражений непосредственно в текст.

    public class MyClass
    {
    <#
    for (int i=0;i < 2;i++)
    {
    #>
      public void DoAction<#=i#>()
      {
      }
    <#
    }
    #>
    }
            

    Здесь выражение <#=i#> подставляет значение i в текст шаблона. Значение i меняется циклически. Результатом работы шаблона будет следующий код.

    public class MyClass
    {
      public void DoAction0()
      {
      }
      public void DoAction1()
      {
      }
    }
            

    Разумеется, выражение может выглядеть гораздо сложнее. Альтернативой блокам выражений является применение методов Write и WriteLine в блоке кода.

    public class MyClass
    {
    <#
    for (int i=0;i < 2;i++)
    {
      WriteLine("\tpublic void DoAction" + i + "()");
    #>
      {
      }
    <#
    }
    #>
    }
            

    Здесь символ "\t" означает табуляцию. Результат будет тем же, что и в предыдущем примере.

    Блоки вспомогательных методов

    Блоки вида <#+…#> позволяют добавлять вспомогательные методы (функции) в шаблон. Они могут впоследствии вызываться изнутри шаблона, где это необходимо. Рассмотрим следующий пример.

    public class MyClass
    {
    <#
    for (int i=0;i < 2;i++)
    {
    #>
      public void <#= CreateFunctionName(i)#>()
      {
      }
    <#
    }
    #>
    }
    <#+ public string CreateFunctionName(int counter)
    {
      string result = "";
      switch (counter)
      {
        case 0:
          result = "DoActionOne";
          break;
        case 1:
          result = "DoActionTwo";
          break;
      }
      return result;
    }
    #>
            

    В цикле вызывается метод CreateFunctionName. Он объявляется в блоке вспомогательных выражений между тегами <#+ и #>.

    Вспомогательные методы обязательно должны находиться в конце шаблона. Блок вспомогательных методов может содержать также блоки выражений, кода, текста - как и в самом шаблоне. Результатом работы примера будет следующее:

    public class MyClass
    {
      public void DoActionOne()
      {
      }
      public void DoActionTwo()
      {
      }
    }
            

    Директивы

    Директивы служат для установки правил обработки конкретного файла шаблона. Синтаксис директив имеет следующий формат:

    <#@ ИмяДирективы [ИмяАттрибута = "ЗначениеАттрибута"] ... #>
            

    Каждая из директив имеет свое предназначение. Имя директивы указывается один раз в самом начале и принимает одно из следующих значений:

  • Template,
  • Parameter,
  • Output,
  • Assembly,
  • Import,
  • Include.
  • Также можно создавать пользовательские директивы. Секция [ИмяАттрибута = "ЗначениеАттрибута"] может указываться один и более раз. Давайте рассмотрим директивы поподробнее.

    Директива template

    Директива Template указывает среде правила обработки файла шаблона и имеет следующий формат:

    <#@ template [language="VB"] [hostspecific="true"] [debug="true"] [inherits="templateBaseClass"]
      [culture="code"] [compilerOptions="options"] #>
                

    Все атрибуты являются необязательными. Параметр language указывает язык, на котором пишется код в шаблоне. Может принимать значения "C#" или "VB". Атрибут compilerOptions определяет условия компиляции. Атрибут culture указывает на культурную среду. Атрибут debug указывает, нужна отладка или нет. С помощью атрибута inherits можно указать, что код файла шаблона может наследоваться от другого класса, который, в свою очередь, тоже может быть сгенерирован из другого шаблона.

    Установка атрибута hostspecific в значение true позволяет коду шаблона получить доступ к API Visual Studio. В коде шаблона становится возможным указывать относительный путь к файлам проекта, а не абсолютный. Также можно получить доступ из шаблона к свойствам класса Host, например, к дате и времени:

    <#= Host.CurrentDateTime #>
                

    Рассмотрим следующую директиву

    <#@ template language="С#" hostspecific="true" debug="true" #>
                

    Эта директива указывает, что код шаблона будет реализован на языке C# и нужен доступ к API Visual Studio. Включается отладчик для поиска и устранения ошибок в шаблоне.

    Директива parameter

    Директива parameter позволяет указывать параметры, которые могут передаваться в шаблон извне, и имеет следующий формат:

    <#@ parameter type="ИмяТипа" name="Имя параметра" #>
                

    Директива parameter доступна только начиная с Visual Studio 2010/.NET 4. Однако, во всех версиях Visual Studio можно передавать параметры в шаблон добавлением значений в класс CallContext.

    Директива parameter имеет обязательные атрибуты type и name. Атрибут type передает название типа параметра. При этом должно указываться полное имя типа, например System.string, а не string. Этот пример передает строковый параметр TableName:

    <#@ parameter type="System.string" name="TableName"#>
                

    После включения параметров их можно использовать внутри шаблона как переменные.

    Директива output

    Директива output позволяет указать расширение и кодировку выходного файла. Расширение можно указать любое, по умолчанию принимается значение ".cs". Кодировка может принимать такие значения, как "utf-8", "us-ascii", "utf-32" и т.д. Имеет следующий формат.

    <#@ output extension=".расширение" [encoding="обозначениеКодировки"] #>
                

    Директива assembly

    Директива assembly позволяет загрузить сборку и применять классы и методы этой сборки в шаблоне. Если нужна сборка из состава .Net, то можно просто указать имя сборки. Для других сборок надо указать полный путь к файлу сборки. Формат директивы assembly следующий:

    <#@ assembly name="[ИмяСборки|ПутьКФайлу]" #>
                

    Директива import

    Формат директивы import:

    <#@ import namespace="ПространствоИмен" #>
                

    Эта директива позволяет импортировать пространства имен. В коде шаблона можно будет использовать простые имена классов вместо полных имен.

    Директива include

    Формат директивы include:

    <#@ include file="filePath" #>
                

    Эта директива позволяет включить в шаблон содержимое другого файла. Включаемый файл может содержать как шаблонный текст, так и команды обработки. Также он может иметь расширение, отличное от ".tt", например ".txt" или ".t4". Если в двух файлах шаблона есть схожие участки, можно в отдельном файле хранить общую часть шаблонов. А в оригинальных файлах директивы включения надо поставить вместо повторяющихся участков кода. Позже, при необходимости можно менять только содержимое включаемого файла.

    Пример чтения из XML-файла

    В шаблонах можно считывать метаданные из XML-файлов и генерировать файлы. Допустим, нам надо создать код объявлений констант. Создадим файл variables.xml и добавим его в проект, заполнив таким содержимым:

    <?xml version="1.0" encoding="utf-8" ?>
    <constants>
      <constant name="A" value="1"/>
      <constant name="B" value="2"/>
      <constant name="C" value="3"/>
      <constant name="D" value="4"/>
    </constants>
        

    Следующий шаблон считывает данные из файла метаданных и генерирует код:

    <#@ template debug="false" hostspecific="true" language="C#" #>
    <#@ output extension=".cs" #>
    <#@ assembly name="System.Xml" #>
    <#@ import namespace="System.Xml" #>
    <#
    XmlDocument reader = new XmlDocument();
    reader.Load(this.Host.ResolvePath("variables.xml"));
    XmlElement elem =reader.DocumentElement;
    for (int i = 0; i < elem.ChildNodes.Count; i++)
    {
        XmlElement elemField = (XmlElement)elem.ChildNodes[i];
    #>
    const int <#=elemField.GetAttribute("name")#> = <#=elemField.GetAttribute("value")#>;
    <#
    }
    #>
        

    Установка параметра hostspecific в значение true позволяет применить выражение this.Host.ResolvePath("variables.xml"). Директива assembly дает возможность использовать в шаблоне классы для работы с XML. Директива import позволяет использовать сокращенные имена классов, например можно писать XmlDocument вместо System.Xml.XmlDocument. Параметр extension директивы output устанавливает расширение выходного файла. Также внутри шаблонного текста используются блоки выражений <#= #>. Результат работы шаблона будет таким:

    const int A = 1;
    const int B = 2;
    const int C = 3;
    const int D = 4;
        

    Как работают шаблоны T4

    Механизм T4 выполняет генерацию в два шага. На первом шаге происходит разбор шаблона и генерируется класс GeneratedTextTransformation, который содержит метод TransformText. Выполняется компиляция этого класса в сборку. На втором шаге создается объект данного класса и запускается метод TransformText, который и формирует текст. Другими словами, на первом шаге из шаблона генерируется программа, на втором шаге эта программа запускается и генерирует выходной файл.

    Контроль отступов в генерируемом коде

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

    public class MyClass
    {
      public void DoAction0()
      {
      }
      public void DoAction1()
      {
      }
    }
        

    Методы PushIndent, PopIndent, ClearIndent позволяют гибко контролировать отступы в генерируемом тексте. Метод PushIndent устанавливает отступ для следующих за ним строк. К началу каждой строки генерируемого текста добавляется символьная строка. Ее содержимое передается в этот метод. Отступом может быть не только табуляция, но и любая последовательность символов. Метод PushIndent может вызываться много раз. После каждого вызова метода строка символов добавляется в стек. Метод PopIndent убирает последний добавленный отступ. Метод ClearIndent очищает стек отступов.

    Рассмотрим пример, при котором нам надо отобразить следующую таблицу из четырех ячеек в формате HTML:

    <table>
      <tr>
        <td>1</td>
        <td>2</td>
      </tr>
      <tr>
        <td>3</td>
        <td>4</td>
      </tr>
    </table>
        

    Шаблон T4, формирующий HTML-разметку:

    <#@ template language="C#" #>
    <#@ output extension=".html" #>
    <#
    WriteLine("<table>");
    PushIndent("\t");
    WriteLine("<tr>");
    PushIndent("\t");
    WriteLine("<td>1</td>");
    WriteLine("<td>2</td>");
    PopIndent();
    WriteLine("</tr>");
    WriteLine("<tr>");
    PushIndent("\t");
    WriteLine("<td>3</td>");
    WriteLine("<td>4</td>");
    PopIndent();
    WriteLine("</tr>");
    ClearIndent();
    WriteLine("</table>");
    #>
        

    Обратите внимание, что нет необходимости добавлять табуляцию в каждом вызове метода WriteLine. Табуляция добавляется в стек отступов в методе PushIndent.

    Генерация текста из моделей DSL и UML

    Модель генерируемой области может быть заложена в базе данных, текстовом файле, XML-файле, файле Visio, а также в диаграммах UML и языках DSL. Чтение данных из XML-файла мы уже рассматривали. Так же достаточно стандартно читаются данные из текстового файла и баз данных. А сейчас мы кратко рассмотрим особенности генерации текста в T4 из моделей UML и DSL.

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

    Существуют три способа генерации файлов из модели UML с применением шаблонов T4:

  • Генерация файлов из меню команд. Создается команда Visual Studio, которая запускается для моделей UML.
  • Генерация файлов из приложения. Создается приложение, которое читает модели UML и генерирует файлы.
  • Генерация времени построения. Файлы генерируются из модели UML внутри проекта Visual Studio.
  • DSL (Domain-Specific Language), или язык предметной области, является нотацией, чаще всего графической, разработанной для конкретных, определенных целей. Для каждой специфической задачи создается язык DSL, учитывающий ее особенности. Эта технология представляет собой противоположность языкам вроде UML, которые являются языками общего назначения, "на все случаи жизни".

    DSL доступен в Visual Studio 2010 через меню добавления нового проекта. Технология DSL позволяет создавать описание предметной области. Создавая шаблоны T4 и передавая метаданные предметной области можно генерировать разного рода тексты, программы и т.д.

    Возможности расширения функциональности T4

    Есть несколько способов расширить функциональность T4:

  • Создание собственного хоста в Visual Studio, своего рода надстройку над ним. Шаблоны T4 должны выполняться внутри некоего хоста, являющегося программой. Visual Studio уже предоставляет хост по умолчанию, однако вы можете создать свой и расширить возможности генерации кода.
  • Предоставлением нового базового класса могут быть расширены возможности шаблонов T4. Во время генерации кода шаблон вызывает методы из этого класса, чтобы собирать информацию, которая будет использована при генерации кода.
  • Создание пользовательских директив и применение их в шаблоне.
  • Плюсы применения шаблонов T4

  • Можно весьма эффективно расширять возможности T4, добавляя пользовательские хосты, директивы. Этими способами видоизменяется или усовершенствуется поведение T4.
  • Генерируется не только код, но и любая текстовая информация.
  • Шаблоны наглядны и просты для понимания. Они эффективны при большом объеме не меняющегося стандартного кода и малом числе настраиваемых параметров.
  • Минусы применения шаблонов T4

  • Один шаблон изначально предназначен для создания только одного файла, хотя это ограничение можно и обойти.
  • Шаблоны T4 поддерживают только два языка - C# и Visual Basic.
  • Шаблоны могут оказаться малоэффективными, если в генерируемом коде мало стандартных участков и много параметров, от которых зависит содержимое сгенерированного кода. В некоторых подобных случаях шаблоны по сложности могут превзойти программы, генерирующие код путем соединения строк текста.
  • Программисты сильно зависят от того, как именно Microsoft будет поддерживать технологию T4 в будущих версиях Visual Studio.
  • Заключение

    Шаблон T4 является, по сути дела, текстом. И его можно хранить в файле или базе данных. Используя набор разных шаблонов, и применяя включение шаблонов, можно строить сложные схемы генерации кода. Технология Т4 может расширяться путем добавления новых возможностей. Наиболее эффективно применение шаблонов в тех случаях, когда большой объем сгенерированного кода остается неизменным и в генератор передается мало параметров.

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

    Вернуться к учебному плану