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

    Приветствую, Хабр!

    Сегодня мы поговорим о рутине. Время от времени каждому программисту приходится совершать много нудной, объемной и шаблонной работы, которую постоянно так и хочется автоматизировать, да руки не доходят. Вот об одном малоизвестном способе упростить себе жизнь с помощью кодогенерации я и хочу сегодня рассказать сообществу дотнетчиков. Способ известен как 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 в виде плагина к Студии. От предыдущего отличается лишь тем, что функционал его бесплатной редакции немного сильнее урезан.

    Заключение


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

    Комментарии 23

      +8
      В каждой достаточно сложной системе реализована с нуля половина LISP'а.
        0
        спасибо, отличное описание
        читал где-то, что первоначально, T4 использовался для внутренних нужд Microsoft. Но технология вышла очень удачной и ее открыли всем.
          0
          А есть ли шаблон для генерации бизнес-объектов? Ну чтобы автоматом генерировалась поддержака интерфейсов IEditableObject, IRevertibleChangeTracking и INotifyPropertyChanged? Написать универсальное решение без рефлексии не получится, а самому копипастить не хочется.

          Я конечно однажды такой шаблон сам напишу, но всё ещё надеюсь найти уже готовый.
          +1
          всегда интересовала тема кодогенерации. Во времена DOS когда я писал на FoxPro я для себя написал кодогенератор на основе описания структуры базы данных и был просто в восторге когда мне макрос сгенерировал код в сотню строк для поддержки переноса данных из одной базы данных с 4-мя связанными таблицами в другую. С использованием кодогенерации количество ошибок в моем проекте тогда резко упало и не удивительно, ведь мне достаточно было подправить макрос генератора, чтоб исправить ошибку во всех местах где он использовался.

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

          То что есть такой штатный инструмент — это просто здорово.
            0
            все таки, мне кажется, кодогенерация — не есть хорошо, а скорее недостаток business фреймворка и т.п., по идее ведь наследования должно хватать, неужели DevForce и StrataFrame тоже кодогенерят? XAF? да еще и в таких количествах как для CSLA, но используем, то что есть и зарекомендовало себя, как индикатор — dice.com
              0
              так то даже штатный редактор WinForms генерит
                0
                наверно, в business frameworks для ОО СУБД такого нет?
                достаточно однажды обозначить сущности

                DevArt Entity Developer для NH радует, делает сказку былью :) хотя бы через генерацию где-то там в фоне
                еще бы шаблоны CSLA прикрутить
              +1
              T4 — а название похоже дали, глядя на аналогиичный инструмент — M4
              0
              Спасибо автору за обзор.
              Просто был у меня проект, в котором у бизнес объектов было по десятку, а то и больше, свойств, да и самих объектов было не мало. В результате приходилось писать кучу строк однообразного кода. Знать бы в то время, что есть такая полезная вещь — жить было бы гораздо проще :)
                0
                Довольно интересный обзор.
                А чем принципиальным T4 отличается от R#? Только тем, что в T4 код генерируется уже при сохранении шаблона *.tt? Или T4 просто облегчает труд и все?
                  0
                  Интересный обзор. У MicroSoft мощная среда разработки что и говорить.
                  А для других языков кто небудь встречал кодогенераторы.
                  Например perl/python/php?
                    –1
                    Уродливо же!
                      0
                      BOOST_PP_FOREACH :-) рулит :)
                        0
                        Спасибо за обзор. Интересно было бы узнать, как сынтегрировать T4 в свой проект и генерировать файлы, когда заранее неизвестно их количество (прослойка к базе, например).
                          0
                          о!
                          скоро в сш появятся нормальные compile time шаблоны. здравствуйте, статический полиморфизм и с++ :)

                          а вообще, если честно, выглядит как попытка «обойти» либо архитектурные сложности, либо неправильный выбор языка.
                          Взрослый MDA+code generation выглядит на порядок красивше имхо.
                            0
                            Большое спасибо автору. Раньше я использовал для генерации кода собственные программки, которые запускались посредством MSBuild, а с T4 так извращаться не нужно.
                              0
                              Спасибо! Очень круто. Для этих нужд нами были были написаны две шарповые и одна перловая утилиты. Жаль, раньше не откопали.
                                0
                                глаза лопаются от всех этих бесконечных выводимых "\t\t\t\t".

                                Это не MS виноват, это в вашем шаблоне текст так забит, а генератор ничего не трогает. Если вы вставили несколько знаков табуляции, так и будет.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое