Шаблоны T4

Управление сложностью решений на основе генерации кода по шаблонам T4

Питер Вогел

Продукты и технологии:

Microsoft .NET Framework, Text Template Transformation Toolkit (T4), Visual Studio 2010

В статье рассматриваются:

  • процесс генерации кода по шаблонам T4;
  • механизмы рефакторинга кода;
  • применение T4 в период выполнения;
  • предупреждения и ошибки.

В своей статье «Lowering the Barriers to Code Generation with T4» в номере «MSDN Magazine» за апрель этого года (msdn.microsoft.com/magazine/hh882448) я описал, как Microsoft Text Template Transformation Toolkit (T4) значительно упрощает разработчикам создание решений в области генерации кода (и как приступить к распознаванию возможности генерации кода в тех или иных приложениях). Однако, как и в случае любой среды программирования, T4-решения могут стать сложными и монолитными, не поддающимися сопровождению или расширению. Чтобы избежать такой участи, вы должны понимать различные способы рефакторинга кода в T4-решениях и его интеграции в другие решения с генерацией кода. А это требует знания процесса генерации кода по шаблонам T4.

Процесс генерации кода по шаблонам T4

Сердцевиной этого процесса является механизм T4, который принимает T4-шаблон, состоящий из стереотипного кода, управляющих блоков, средств классов и директив. На основе этих элементов механизм T4 создает временный класс («сгенерированный класс преобразования»), производный от класса Microsoft TextTransformation. Затем создается домен приложения, и сгенерированный класс преобразования в этом домене компилируется и выполняется, давая тот или иной вывод, который может быть чем угодно — от HTML-страницы до программы на C#.

Стереотипный код и управляющие блоки в шаблоне объединяются в один метод (с именем TransformText) в сгенерированном классе преобразования. Однако код в средствах класса (class features) (заключенный в разделители <#+…#>) в этот метод не помещается — такой код добавляется в сгенерированный класс преобразования вне любых методов, создаваемых T4-процессом. Например, в предыдущей статье я использовал блок средств класса, чтобы добавить ArrayList (объявленный вне любого метода) к сгенерированному классу преобразования. Потом я обращался к этому ArrayList в управляющих блоках генерации кода как к части процесса, генерирующего код. Помимо добавления полей вроде ArrayList, средства класса можно использовать для включения закрытых методов, которые можно вызывать из ваших блоков кода.

Механизм T4, конечно, является сердцевиной, но в процессе генерации кода участвуют и два других компонента: процессор директив и хост. Директивы позволяют добавлять код или иным образом управлять процессом на основе параметров. Например, T4-директива Import имеет параметр Namespace для создания выражения using или Imports в сгенерированном классе преобразования; директива Include включает параметр file для извлечения текста из другого файла и его добавления в сгенерированный класс преобразования. По умолчанию процесс директив обрабатывает те директивы, которые входят в состав T4.

Процессу генерации кода по шаблонам T4 также нужен хост для интеграции процесса со средой, в которой работает механизм T4. Хост, например, предоставляет механизму стандартный набор сборок и пространств имен, чтобы вам не приходилось указывать в шаблоне все сборки, необходимые коду сгенерированного класса преобразования. По запросу механизма T4 или процессора директив хост добавляет ссылки на сборки, извлекает (а иногда и читает) файлы для механизма и может даже получать пользовательские процессоры директив или передавать значения по умолчанию для директив, чьи параметры были опущены. Хост также предоставляет AppDomain, в котором выполняется сгенерированный класс преобразования, и отображает любые ошибки и предупреждения, генерируемые механизмом T4. Хостом для механизма T4 может быть Visual Studio (через дополнительный инструмент TextTemplatingFileGenerator), а также утилита командной строки TextTransform, которая обрабатывает T4-шаблоны вне Visual Studio.

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

Рис. 1. Пример T4-шаблона

<#@ template language="VB" #>
public partial class ConnectionManager
{
<#
  For Each conName As String in Connections
#>
  private void <#= conName #>(){}
<#
  Next
#>
<#+
  Private Function GetFormattedDate() As String
    Return DateTime.Now.ToShortDateString()
  End Function
#>

Сгенерированный класс преобразования будет выглядеть примерно так, как показано на рис. 2.

Рис. 2. Сгенерированный класс преобразования

Public Class GeneratedTextTransformation
  Inherits Microsoft.VisualStudio.TextTemplating.TextTransformation
  Public Overrides Function TransformText() As String
    Me.Write("public partial class ConnectionManager{")
    For Each conName As String in Connections
      Me.Write("private void ")
      Me.Write(Me.ToStringHelper.ToStringWithCulture(conName))
      Me.Write("(){}")    
    Next
  End Function
  Private Function GetFormattedDate() As String
    Return DateTime.Now.ToShortDateString()
  End Fuction
End Class

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

  • средства класса (class features);
  • расширение базового класса TextTransformation;
  • пользовательские процессоры директив (custom directive processors).

Эти механизмы также позволяют повторно использовать код между несколькими решениями. Чтобы продемонстрировать эти варианты, я воспользуюсь тривиальным примером: добавление ссылки на авторские права (copyright notice) в генерируемый код. («Метафизичность» написания кода, который генерирует другой код, создает достаточные трудности даже без выбора более сложных примеров.)

Существует, как минимум, еще один вариант, который я не стану здесь рассматривать: разработка собственного хоста, чтобы получить возможность вызывать специфичные для него средства из своего T4-шаблона. Создание нового хоста на самом деле требуется, только если вы намерены запускать T4-обработку извне Visual Studio и не хотите использовать утилиту командной строки TextTransform.

Средства класса

Средства класса (class features) предоставляют простой способ уменьшения сложности в вашем T4-процессе и повторном использовании кода. Они позволяют инкапсулировать части процесса генерации кода в методы, которые можно вызывать из метода TransformText, образующего «основную ветвь» вашего кода. Вы также можете использовать директиву Include для многократного применения средств класса в нескольких решениях.

Первый шаг в использовании средства класса — добавление T4-файла в проект, содержащий средства класса, которые нужно многократно применять в нескольких решениях с генерацией кода; при этом каждое средство заключается в T4-разделители <#+…#>. Средства класса могут включать методы, свойства и поля. Ваши файлы шаблонов также могут содержать T4-специфичные средства, например директивы. Поскольку ваш по-прежнему является T4-файлом, он будет генерировать код, который будет компилироваться в вашем приложении, поэтому вы должны отключить генерацию кода для этого файла. Самый простой способ сделать это — очистить свойство Custom Tool ваших файлов средств класса. В следующем примере метод ReturnCopyright определяется как средство класса, написанное на Visual Basic:

<#+
 Public Function ReturnCopyright() As String
   Return "Copyright by PH&V Information Services, 2012"
 End Function
#>

Определив средство класса, вы можете добавить его в шаблон, используя директиву Include, и применять это средство из управляющего блока в T4-шаблоне. В следующем примере (в нем предполагается, что предыдущее средство класса было определено в файле с именем CopyrightFeature.tt) метод ReturnCopyright используется как выражение:

<#@ Template language="VB"   #>
<#@ Output extension=".generated.cs" #>
<#= ReturnCopyright() #>
<#@ Include file="CopyrightFeature.tt" #>

В качестве альтернативы можно просто применять обычный стереотипный синтаксис T4 для генерации похожего кода:

<#+
  Public Function ReturnCopyright() As String
#>
  Copyright by PH&V Information Services, 2012
<#+
  End Function
#>

Для генерации кода вы также можете вызывать методы Write и WriteLine внутри средства класса:

<#+
  Public Sub WriteCopyright()
    Write("Copyright by PH&V Information Services, 2012")
  End Function
#>

В последних двух подходах после добавления директивы Include в шаблон для вызова метода использовался бы такой код:

<# WriteCopyright() #>

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

По сравнению с другими решениями применение средств класса имеет минимум одно преимущество и один недостаток. Вы получаете выигрыш, когда разрабатываете свое решение с генерацией кода: средства класса (и Includes в целом) не входят в скомпилированные сборки. По соображениям производительности механизм T4 может блокировать сборки, используемые им сборки при загрузке сгенерированного класса преобразования в домен приложения. То есть при тестировании и модификации скомпилированного кода вы можете обнаружить, что соответствующие сборки нельзя заменить без закрытия и перезапуска Visual Studio. Со средствами класса такого не происходит. Заметьте, что эта проблема была устранена в Visual Studio 2010 SP1, где сборки больше не блокируются.

Однако разработчики могут столкнуться и с недостатком при использовании вашего решения с генерацией кода: им придется добавлять в свой проект не только ваш T4-шаблон, но и ваши вспомогательные файлы T4 Include. Это тот случай, где вам следует подумать о создании шаблона Visual Studio, содержащего все T4-файлы, которые понадобятся разработчику при использовании вашего решения с генерацией кода; в таком варианте все нужные файлы могут быть добавлены как группа. Также имело бы смысл выделять все свои Include-файлы в некую папку внутри решения. В следующем примере с помощью директивы Include из папки с именем Templates добавляется файл, содержащий средства класса:

<#@ Include file="Templates\classfeatures.tt" #>

Разумеется, вы не ограничены в Include-файлах использованием только средств класса — можно включать произвольный набор управляющих блоков и текста, которые вам нужно вводить в метод TransformText генерируемого класса преобразования. Однако по аналогии с операторами GoTo, которых следует избегать, использование тщательно продуманных членов в Include-файлах помогает управлять концептуальной сложностью вашего решения.

Расширение класса TextTransformation

Замена класса TextTransformation собственным позволяет включать нужную вам функциональность в методы этого класса, которые можно вызывать в сгенерированном классе преобразования. Расширение класса TextTransformation — хороший вариант, когда у вас есть код, используемый во многих (или всех) ваших решениях с генерацией кода: по сути, вы выделяете этот код из своих решений и переносите его в механизм T4.

Предупреждения и ошибки

Любой надежный процесс уведомляет о своем прогрессе. В средстве класса или в расширении класса TextTransformation вы можете сообщать о проблемах в выполнении сгенерированного класса преобразования, добавив вызовы методов Error и Warning в класс TextTransformation. Оба метода принимают одну строку и передают ее хосту Microsoft Text Template Transformation Toolkit (T4) (хост отвечает за выбор способа отображения сообщений). В Visual Studio эти сообщения появляются в окне Error List.

В следующем примере сообщается об ошибке в средстве класса:

<#= GetDatabaseData("") #>
<#+  private string GetDatabaseData(string ConnectionString)
{
    if (ConnectionString == "")
    {
      base.Error("No connection string provided.");
    }
  return "";
    }
...

В средстве класса Error и Warning можно использовать для отчета об ошибках, которые происходят только при выполнении сгенерированного класса преобразования, но не годятся для уведомления о проблемах в ходе T4-процесса. Иначе говоря, в средстве класса сообщения Error и Warning будут появляться, только если сгенерированный класс преобразования может быть корректно собран из вашего T4-файла, скомпилирован и запущен.

Если вы создаете собственный процессор директив, то все равно можете интегрировать его с T4-процессом отчета об ошибках, добавив объекты Compiler¬Error в свойство Errors класса. Объект CompilerError поддерживает передачу нескольких частей информации об ошибке (в том числе номер строки и имя файла), но в следующем примере просто задается свойство ErrorText:

System.CodeDom.Compiler.CompilerError err =  new 
System.CodeDom.Compiler.CompilerError();
err.ErrorText = "Missing directive parameter";
this.Errors.Add(err);

Первый шаг в расширении класса TextTransformation — создание абстрактного класса, который наследует от следующего класса:

public abstract class PhvisT4Base:
  Microsoft.VisualStudio.TextTemplating.TextTransformation
{
}

Если у вас нет Microsoft.VisualStudio.TextTemplating.DLL, содержащей класс TextTransformation, скачайте SDK для своей версии Visual Studio, прежде чем добавлять ссылку.

В зависимости от версии T4 вам, возможно, придется позаботиться о реализации метода TransformText при наследовании от TextTransformation. В этом случае ваш метод должен возвращать строку, содержащую вывод сгенерированного класса преобразования, который хранится в свойстве Generation­Environment класса TextTransformation. Ваше переопределение метода TransformText, если оно необходимо, должно выглядеть так:

public override string TransformText()
{
  return this.GenerationEnvironment.ToString();
}

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

protected string Copyright()
{
  return @"Copyright PH&V Information Services, 2012";
}

Этот метод можно использовать либо в выражении, либо совместно с T4-методами Write и WriteLine, например:

<#= CopyRight() #>
<#  WriteLine(CopyRight()); #>

В качестве альтернативы можно использовать метод Write или WriteLine базового класса TextTransformation непосредственно в методах, которые вы добавляете к этому базовому классу:

protected void Copyright()
{
  base.Write(@"Copyright PH&V Information Services, 2012");
}

После этого данный метод можно вызывать из управляющего блока:

<# CopyRight(); #>

Последний шаг в замене исходного класса TextTransformation — задание в T4-шаблоне того, что сгенерированный класс преобразования должен наследовать от вашего нового класса. Для этого вы используете параметр Inherits атрибута Template:

<#@ Template language="C#" inherits="PhvT4Utils.PhvisT4Base" #>

Вы также должны добавить в ваш T4-шаблон директиву Assembly, которая ссылается на DLL с вашим базовым классом. Для этого используйте полный путь к DLL:

<#@ Assembly name="C:\T4Support\PhvT4Utils.dll" #>

В качестве альтернативы можно добавить свой базовый класс в кеш глобальных сборок (GAC).

В Visual Studio 2010 можно использовать переменные окружения и макросы для упрощения пути к DLL. Например, при разработке вы могли бы задействовать макрос $(ProjectDir), чтобы ссылаться на DLL со своим базовым классом:

<#@ Assembly name="$(ProjectDir)\bin\PhvT4Utils.dll" #>

В период выполнения, если исходить из того, что вы устанавливаете свой файлы классов в папку Program Files, можно использовать в 32-разрядных версиях Windows переменную окружения %programfiles%, а в 64-разрядных версиях Windows — %ProgramW6432% для папки Program Files или %ProgramFiles(x86)% для папки Program Files (x86). В следующем примере предполагается, что DLL размещена в C:\Program Files\PHVIS\T4Tools в 64-разрядной версии Windows:

<#@ Assembly name="%ProgramW6432%\PHVIS\T4Tools\PHVT4Utils.DLL" #>

Как и в случае других решений с компилируемым кодом, Visual Studio может блокировать DLL с вашим кодом при выполнении сгенерированного класса преобразования. Если это произойдет, когда вы разрабатываете свой класс TextTransformation, вам придется перезапустить Visual Studio и перекомпилировать код, прежде чем вы сможете вносить дальнейшие изменения.

Пользовательские процессоры директив

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

Главная проблема с процессорами директив в том, что вы должны добавлять раздел в реестр Windows, чтобы сторонний разработчик мог задействовать ваш процессор. И здесь вновь стоит подумать об упаковке T4-решения, чтобы необходимая запись в реестре создавалась автоматически. В 32-разрядных версиях Windows этот раздел должен быть таким:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\
visualstudioversion\TextTemplating\DirectiveProcessors

В 64-разрядных версиях Windows тот же раздел должен быть следующим:

HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\
VisualStudio\10.0\TextTemplating\DirectiveProcessors

Название вашего раздела должно совпадать с именем вашего класса процессора директив, которое (по соглашению об именовании Microsoft) должно заканчиваться словосочетанием «DirectiveProcessor». Вам потребуются следующие параметры в этом разделе реестра.

  • Default Пустой параметр или с описанием вашей директивы.
  • Class Полное имя вашего класса в формате ПространствоИмен.ИмяКласса.
  • Assembly/CodeBase Имя вашей DLL, если вы поместили DLL своей директивы в GAC (Assembly) или полный путь к DLL этой директивы (CodeBase).

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

Для использования вашего процессора директив сторонний разработчик добавляет директиву с любым именем в T4-шаблон и связывает эту директиву с вашим процессором через параметр Processor директивы (который ссылается на нужный раздел реестра Windows). При выполнении T4-шаблона вашему процессору директив передается имя директивы, которую сторонний разработчик использовал, наряду с любыми указанными им параметрами.

В этом примере директива Copyright связывается с процессором CopyrightDirectiveProcessor и в нее включается параметр Year:

<#@ Copyright Processor="CopyrightDirectiveProcessor" Year=”2012” #>

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

<#= Copyright #>

Конечно, следующий шаг состоит в создании процессора директивы для обработки этой директивы.

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

T4-процессоры директив наследуют от класса Microsoft.VisualStudio.TextTemplating.DirectiveProcessor (скачайте Visual Studio SDK, чтобы получить библиотеку TextTemplating). Ваш процессор директив должен возвращать из своего метода GetClassCodeForProcessingRun код, который будет добавляться в сгенерированный класс преобразования. Однако перед вызовом метода GetClassCodeForProcessingRun механизм T4 будет вызывать метод IsDirective­Supported процессора (с передачей имени вашей директивы) и метод ProcessDirective (с передачей имени директивы и значений ее параметров). Метод IsDirectiveSupported должен возвращать false, если вашу директивы не следует выполнять, и true в ином случае.

Применение T4 в период выполнения

Вы можете получить представление о том, как выглядит сгенерированный класс преобразования, используя Preprocessed Text Templates, которые позволяют генерировать текст в период выполнения. Компиляция или выполнение результатов генерации кода Microsoft Text Template Transformation Toolkit (T4) в период выполнения, по-видимому, не является тем, с чем захочет возиться большинство программистов. Однако, если вам нужно несколько «похожих, но разных» версий XML- или HTML-документа (либо другого текста), то Preprocessed Text Templates позволяют использовать T4 для генерации этих документов в период выполнения.

Как и в случае других T4-решений, первый шаг в использовании T4 в период выполнения — добавление T4-файла в проект из диалога New Item. Но вместо добавления файла Text Template вы добавляете Preprocessed Text Template (также перечисляемый в диалоге New Item в Visual Studio). Preprocessed Text Template идентичен стандартному файлу T4 Text Template с тем исключением, что свойство Custom Tool задается как TextTemplatingFileProcessor вместо обычного TextTemplatingFileGenerator.

В отличие от случая с Text Template дочерний файл, содержащий сгенерированный код из Preprocessed Text Template, не включает вывод кода из сгенерированного класса преобразования. Вместо этого файл содержит нечто очень похожее на один из ваших сгенерированных классов преобразования: класс с тем же именем, что и файл Preprocessed Text Template, с методом TransformText. Вызов этого метода TransformText в период выполнения возвращает в виде строки то, что вы ожидали бы найти в файле кода T4-шаблона: ваш сгенерированный код. Таким образом, для файла Preprocessed Text Template с именем GenerateHTML вы получили бы его сгенерированный текст, используя такой код в период выполнения:

GenerateHTML HtmlGen = new GenerateHTML();
string html = HtmlGen.TransformText();

Поскольку методу ProcessDirective передается вся информация о директиве, именно в нем вы обычно формируете код, который будет возвращаться методом GetClassCodeForProcessingRun. Вы можете извлекать значения параметров, указанные в директиве, считывая их из второго параметра метода (эти значения называются аргументами). Следующий код в методе ProcessDirective ищет параметр Year и использует его для формирования строки, содержащей объявление переменной. Потом строка возвращается из GetClassCodeForProcessingRun:

string copyright = string.Empty;
public override void ProcessDirective(
  string directiveName, IDictionary<string, string> arguments)
{
  copyright = "string copyright " +
              "= \"Copyright PH&V Information Services, " +
              arguments["Year"] +"\";";
}
public override string GetClassCodeForProcessingRun()
{
  return copyright;
}

Ваш процессор директив может также добавлять ссылки и выражения using/­import в сгенерированный класс преобразования для поддержки кода, добавленного через GetClassCodeForProcessingRun. Чтобы включить ссылку на сгенерированный класс преобразования, нужно просто вернуть имена библиотек в строковом массиве из метода GetReferencesForProcessingRun. Например, если коду, добавляемому в сгенерированный класс преобразования, нужны классы из пространства имен System.XML, вы можете написать такой код:

public override string[] GetReferencesForProcessingRun()
{
  return new string[] {"System.Xml"};
}

Аналогично вы можете указать пространства имен, которые нужно добавить в сгенерированный класс преобразования (в виде выражений using или Imports), вернув строковый массив из метода GetImportsForProcessingRun.

Класс преобразования сгенерированного кода также включает методы, выполняемые до и после инициализации, которые вызываются до метода TransformText. Вы можете возвращать код, который будет добавляться в эти методы, из методов GetPreInitializationCodeForProcessingRun и GetPostInitializationCodeForProcessingRun.

При отладке помните, что использование Run Custom Tool не заставляет Visual Studio собирать ваше решение. Внося изменения в свой процессор директив, не забывайте перед выполнением шаблона заново компилировать решение, иначе эти изменения не вступят в силу. И вновь, поскольку T4 блокирует используемые сборки, вам может понадобиться перезапуск Visual Studio и повторная компиляция кода перед очередным раундом тестирования.

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


Питер Вогел (Peter Vogel) — глава компании PH&V Information Services. Последняя из его книг на данный момент — «Practical Code Generation in .NET» (Addison-Wesley Professional, 2010). PH&V Information Services специализируется на консалтинге в области проектирования архитектур на основе сервисов и на интеграции .NET-технологий в эти архитектуры. Помимо практики в области консалтинга, написал учебный курс по проектированию архитектур, ориентированных на сервисы, в рамках Learning Tree International, который изучают во всем мире.

Выражаю благодарность за рецензирование статьи эксперту Гарету Джонсу (Gareth Jones).