Pull to refresh

Text Template Transformation Toolkit (T4): генератор кода в Visual Studio

Reading time 8 min
Views 28K
Приветствую, Хабр!

Сегодня мы поговорим о рутине. Время от времени каждому программисту приходится совершать много нудной, объемной и шаблонной работы, которую постоянно так и хочется автоматизировать, да руки не доходят. Вот об одном малоизвестном способе упростить себе жизнь с помощью кодогенерации я и хочу сегодня рассказать сообществу дотнетчиков. Способ известен как Text Template Transformation Toolkit или попросту T4.

Знакомство с Т4


Пример задачи


Представьте себе следующую ситуацию: вам необходимо описать некий конечный автомат. Реализован он будет невероятно криво, но пример для иллюстрации вполне подходит. В ходе реализации сердцем автомата стала функция, которая принимает один параметр — текущее состояние, и в зависимости от его значения выполняет те или иные действия. Все возможные состояния автомата вы заблаговременно описали в enum`е, и теперь грустно смотрите на монитор: предстоит описывать гигантский switch на полэкрана, а как же не хочется…

Конкретизируя задачу, пусть в автомате 3 состояния (а вы представьте ситуацию, когда их 43 — скажем, это какое-нибудь сообщение WinAPI), и enum выглядит так:

enum State
{
    Alive,
    Dead,
    Schrodinger
}

Задача — создать с помощью T4 шаблон, который будет генерировать по нему следующий код:

void Select(State state)
{
  switch (state)
  {
      case State.Alive:
          // code here
          break;
      case State.Dead:
          // code here
          break;
      case State.Schrodinger:
          // code here
          break;
      default:
          // code here
          break;
  }
}

Рецепт решения


Здесь и далее я неявно предполагаю, что у вас установлена Visual Studio 2005/2008/2010. В произвольный проект добавляем новый файл с расширением *.tt, к примеру Switch.tt. Это расширение — стандартное для файлов шаблонов и автоматически распознается Студией. Заметьте, в Solution Explorer к нему автоматически добавился ещё один узел, содержащий пока что пустой файл Switch.cs. В нём потом окажется результат генерации.
Итак, сначала — рецепт, потом объяснения. В пустой файл Switch.tt вставим следующий текст:
<#@ template language="C#v3.5" debug="True" #>
<#@ output extension="cs" #>
void Select(State state)
{
    switch (state)
    {
<# foreach (string Value in Enum.GetNames(typeof(State))) { #>
        case State.<#= Value #>:
            // code here
            break;
<# } #>
        default:
            // code here
            break;
    }
}
<#+ 
enum State
{
   Alive,
   Dead,
   Schrodinger
}
#>

После нажатия Ctrl+S стоит всего лишь посмотреть в Switch.cs и мы увидим там необходимый текст.

Как такое волшебство получается?


Взгляните пристальнее на текст шаблона. T4 использует декларативный стиль, поэтому повсеместно в его коде используются теги в стиле ASP.NET. Итак, весь код, который находится в файле шаблона, можно разделить на пять видов:
  • Текстовый блок — не обрамлён никакими тегами, копируется в выходной файл «как есть».
  • Директивы — устанавливают настройки шаблона, используются T4 в процессе генерации. Обрамляются тегами <#@ #>. Ниже я опишу самые используемые из них.
  • Блок кода — этот код после дословно будет вставлен T4 в класс, который собственно и занимается генерацией выходного файла. Обрамляется тегами <# #>.
  • Блок выражения — внедряется внутрь текстового блока. Он содержит некоторое выражение, которое возможно скомпилировать в рамках класса генерации, к примеру, переменную, ранее определённую в каком-нибудь блоке кода. При генерации выходного файла вместо него подставится текущее значение этого выражения. Обрамляется тегами <#= #>.
  • Блок классового свойства — обрамляется тегами <#+ #>. Его предназначение мы обсудим чуть позже.

Чтобы в полной мере осознать принципы, по которым пишутся шаблоны, придётся заглянуть за кулисы: узнать, как именно T4 интерпретирует текст нашего .tt-файла. Для этого найдите в своей папке %TEMP% последний созданный .cs-файл. Если выкинуть многочисленные #line и отформатировать код, то в данном случае он будет выглядеть приблизительно так:

namespace Microsoft.VisualStudio.TextTemplatingFE5E01C975766D0E3C1DD071A5BFF52A {
using System;
using Microsoft.VisualStudio.TextTemplating.VSHost;
 
public class GeneratedTextTransformation : Microsoft.VisualStudio.TextTemplating.TextTransformation {
   public override string TransformText() {
       try {
           this.Write("void Select(State state)\r\n{\r\n\tswitch (state)\r\n\t{\r\n");
           foreach (string Value in Enum.GetNames(typeof(State))) {
               this.Write("\t\tcase State.");
               this.Write(Microsoft.VisualStudio.TextTemplating.ToStringHelper.ToStringWithCulture(Value));
               this.Write(":\r\n\t\t\t// code here\r\n\t\t\tbreak;\r\n");
           } 
           this.Write("\t\tdefault:\r\n\t\t\t// code here\r\n\t\t\tbreak;\r\n\t}\r\n}\r\n");
       }
       catch (System.Exception e) {
           System.CodeDom.Compiler.CompilerError error = new System.CodeDom.Compiler.CompilerError();
           error.ErrorText = e.ToString();
           error.FileName = "C:\\Users\\Alex\\Documents\\Visual Studio 2008\\Projects\\T4Article\\T4Article\\Switch.tt" + "";
           this.Errors.Add(error);
       }
       return this.GenerationEnvironment.ToString();
   }
   enum State
   {
       Alive,
       Dead,
       Schrodinger
   }
}
}

Как видим, T4 автоматически создаёт класс, наследник Microsoft.VisualStudio.TextTemplating.TextTransformation, и переопределяет в нём один-единственный метод TransformText(), который и формирует по частям текст выходного файла. Блоки кода из исходного шаблона становятся частью кода метода, а текстовые блоки добавляются в выходной текст посредством вызовов this.Write. Аналогичным образом интерпретируются блоки выражения. Что же касается блоков классовых свойств, то, как теперь видно, они представляют собою ту информацию, которая будет добавлена потом внутрь класса-генератора, чтобы в коде метода TransformText() можно было на неё ссылаться. В нашем примере это определение enum`а State, но вы прекрасно можете описать внутри <#+ #>, к примеру, функцию, которую будет активно использовать генератор.

Генератор написан на C#. Почему? Потому что мы так приказали. Директива <#@ template language="C#v3.5" #> задаёт язык программирования, который используется в блоках кода, классовых свойств и выражений. На данный момент у неё всего четыре возможных значения: "C#", "VB", "C#v3.5" и "VBv3.5"
Ещё, к слову, если бы в тексте шаблона не было указано <#@ template debug="True" #>, то никаких следов генератора, равно как и связанной с ним дебаг-информации, вы бы в %TEMP% так и не нашли.

Директивы

  • <#@ assembly name="System.Data" #> прилинкует к проекту генератора сборку System.Data. Работает аналогично «Add Reference» в Visual Studio.
  • <#@ import namespace="System.Diagnostics" #> добавит в генератор ссылку-using на пространство имён System.Diagnostics.
  • <#@ include file="Another.tt" #> вставит в данную точку содержимое шаблона Another.tt.
  • <#@ output extension=".cs" encoding="UTF-8"#> скажет T4, что выходной файл должен быть в кодировке UTF-8 и иметь расширение .cs.
Остальные директивы и параметры используются гораздо реже, поэтому я не считаю нужным тратить на них ваше экранное пространство :) Все необходимые ссылки на документацию любопытные могут найти в постскриптуме.

Кое-что особенное


TextTransformation


Надо признать, что, реализовывая разбор шаблона и создание генератора, ребята из Microsoft использовали свои же реализованные возможности крайне неэкономно. К примеру, возьмём идентацию кода. Генератор, приведённый выше, читать проблематично — глаза лопаются от всех этих бесконечных выводимых "\t\t\t\t".
А ведь всего-то нужно было заюзать простую функцию PushIndent("\t"). Она дописывает новый кусочек к общему префиксу, который потом будет добавляться к каждому Write`у. Как нетрудно догадаться, парная ей функция PopIndent() убирает из префикса последний добавленный туда кусочек.
А ещё есть свойство CurrentIndent и метод ClearIndent(). Просто информации ради :)

Аналогичная ситуация с переносами строк — вместо того чтобы плодить в строковых литералах "\r\n", можно было просто заменить Write на WriteLine. В рамках текущей платформы, разумеется.

И не мешало бы упомянуть методы Warning() и Error(). Будучи вызванными в коде генератора, они вызовут соответственно предупреждение либо ошибку, которые отобразятся в Error List при попытке интерпретации шаблона. Очень удобно использовать в непредвиденных ситуациях в блоках кода.

Профессиональные примеры


По предыдущей задаче может сложиться впечатление, что шаблоны T4 выгодно использовать только для мелких подручных задач, чтобы не писать много нудного C# кода. Ничего подобного. Яркий иллюстрирующий это пример: шаблон из коллекции Tangible, который по указанной вами папке с изображениями создаёт простую html-страницу с галереей картинок.
Что самое интересное, он недлинный, легко читается и модифицируется под нужды разработчика.
Чтобы не уродовать статью лишними блоками текста, я разместил его на pastebin.

T4-штучки


А в заключение статьи — несколько полезных ссылок.
Ссылка на документацию MSDN по T4 была приведена в начале статьи. Это — наиболее полное описание синтаксиса и возможностей генератора, тем не менее, без каких-либо полезных на практике примеров.

Блог Oleg Sych — кладезь полезной информации по T4. Здесь вы сможете узнать в подробностях о том, как отлаживать и модифицировать шаблоны, найдёте множество готовых примеров (хранимые процедуры, классы LINQ и Entity Framework, конфиги, скрипты MSBuild и WiX и т.д.), а также рассказ о более «продвинутых» возможностях шаблонов, которые не поместились в данную статью. Возможно, я адаптирую эти сведения к хабрааудитории как-нибудь позже, в следующей статье.

T4 Toolbox — набор готовых темплейтов для создания «Нового файла» T4 в Visual Studio.

T4 Editor от Tangible Engineering — удобный редактор с подсветкой и IntelliSense, внедряющийся как плагин к Visual Studio. Доступен в двух редакциях: платной и урезанной бесплатной. Лично мне возможностей бесплатной версии хватает за глаза.
Чем ещё удобен Tangible — своей неплохой галереей готовых шаблонов.

T4 Editor от Clarius Consulting — ещё один редактор для шаблонов T4 в виде плагина к Студии. От предыдущего отличается лишь тем, что функционал его бесплатной редакции немного сильнее урезан.

Заключение


Мораль сей басни такова: если вас судьба поставила перед неизбежностью монотонной и нудной работы, никогда не стоит отказываться от помощи. Особенно — от помощи компьютера.
Удачной вам кодогенерации! :)
Tags:
Hubs:
+26
Comments 23
Comments Comments 23

Articles