Цель статьи: дать пару идей для автоматизации, а может даже и рабочий инструмент для создание T4-болванок под решения типовых задач, производимых с классами/интерфейсами в работе.
Немного конкретики: есть совсем простые, хорошо алгоритмизированные, повседневные задачи — из DTO-класса написать Model, из Model — ViewModel, типовые Тесты или трансформация объекта в Json. Задачи, которые отлично ложатся на механизмы T4, но которые долго или неудобно применять. Разработчик обычно стоит на распутье: выполнить задачу или написать скрипт, который, может быть, поможет ее выполнить.
Иногда скрипт кажется слишком сложным, иногда человека останавливает мысль о том, что скрипт по сути — не функциональная часть проекта, а скрипта-на-один-раз. И сверяя сложность линейной задачи с невыясненной — разработчик часто отказывается от второго пути. Но чем больше у него опыта, тем меньше ему нравится линейный труд. И встает вопрос — как донести возможности T4 для всей команды, не заставляя ее вникать, а предоставляя возможность вникнуть. При том, поддержка должна быть минимальна. Обычные T4 скрипты нуждается в поддержке (что бы быть употребленным еще раз, должен таскаться вместе с разработчиком по проектам, в какой-то момент должен быть «подпиленным» под определенную задачу). И такого хочется избежать.
В идеале же, я полагаю, процесс должен состоять из нажатия волшебной кнопки, которая генерирует нужную болванку автоматически, в зависимости от требуемого сценария. И ее уже разработчик сможет быстро доработать до практической применимости в разрезе конкретной ситуации. Т.е. на самом деле я предлагаю поработать над недостатками особенностей использования скриптов кодегенерации. Качество и время как главные критерии, потому все «волшебство» можно разделить на 3 составляющих:
- Шаблонная генерация отдельного сценария.
- Простая интеграция в процесс разработки.
- Валидация результата.
Теперь по порядку:
Часть 1. T4 генерация мечты.
Проще всего будет показать весь процесс на определенной задаче. MVVM, путешествие сущности из объекта сервиса до объекта верхних уровней.
Часто происходит так, большая часть DTO-объекта попадает в модель с минимально простыми изменениями. Объект транспортного уровня не несет в себе никакой логики, только данные — и модель в минимальном исполнении должна минимум копировать часть его свойств, налагая права доступа к ним. Если применить это к слоям MVVM, выражается это в трансформации:
Итого 2 трансформации. На выходе первого преобразования решение должно дорабатываться человеком, привнося логику. Затем еще одна трансформация, создавая уже более высокую ступеньку.
Думаю, схожую функциональность может обеспечить и CopyType* + AutoMapper. Так же, могут помочь и аспекты (для реализации стандартных реакций). И в простых случаях лучше использовать именно их. Но, мое личное мнение, если mapping перестает быть тривиальным, или отладка превращается в Code Hell — надо упрощать объекты, работая в первую очередь над повышением читаемости кода и механизмы, скрывающие под капотом важную функциональность тут плохие помощники. Повторюсь, это имхо.
Для таких нехитрых преобразований понадобится помощь T4 toolbox ( спасибо Oleg V. Sych) и скрипт, в примитиве представляющий собой следующее:
Просто, но есть несколько "плохих" моментов в использовании.
1. Что бы использовать тип CodeElement/CodeClass/CodeProperty, в месте использования надо оставлять ссылку на библиотеку EnvDTE что не сочетается с потребностями проекта (библиотека относится к среде VisualStudio, будет очень странным добавлять и подчищать ссылки за собой).
2. Источник для преобразования этим скриптом должен быть указан явно.
3. В общем случае не решается вопрос с доступностью полей в сгенерированном коде. Но, по правде сказать, этот вопрос я так и не решил, отдав его на откуп разработчиков, рассудив что стереть setter-ы проще, чем на каждый тип делать схему доступности. Это сильно экономит время, упрощает скрипт, но качество «выхода» снижает.
Первые два недостатка мы можем исправить, перейдя ко второй части:
Часть 2. Интеграция в процесс разработки.
Интегрироваться в процесс разработки безболезненно можно с помощью нескольких инструментов. Было бы очень приятно использовать что-то вроде Resharper-овского контекстного меню, которое на конкретном классе дает выпадающий список из возможных скриптов, но я не нашел способа создать такой плагин, буду раз, если кто подскажет. Однако, плагин к самой студии создать вполне реально.
Для этого понадобится установка Visual Studio SDK и немного времени, для того, что бы разобраться как встраиваться в IDE от Microsoft.
Wizard делает большую часть работы, потому освещать этот процесс не стану, но остановлюсь на 2 важных файлах: .vsct и MenuCommandsPackage.
В первой надо описать плоскую структуру построения меню в формате xml, где каждая кнопка представлена в виде:
<Button guid="guidMenuAndCommandsCmdSet" id="cmdCommand9" priority="0x309" type="Button">
<Parent guid="guidMenuAndCommandsCmdSet" id="MyMenuGroup"/>
<CommandFlag>DynamicVisibility</CommandFlag>
<CommandFlag>DefaultInvisible</CommandFlag>
<CommandFlag>TextChanges</CommandFlag>
<Strings>
<ButtonText>T4 Command</ButtonText>
</Strings>
</Button>
Т.е. в .vsct мы описываем максимально число возможных пунктов меню (далее слотов), в которые мы будем «вставлять» файлы преобразования. Т.к. на этапе установки плагина мы не знаем, сколько скриптов на самом деле нам понадобится, берем их N и делаем невидимыми. А вот уже в MenuCommandsPackage по количеству скриптов формируем команды, ставим им в соответствие слоты и отображаем:
for (int index = 0; index < files.Length; index++)
{
string file = files[index];
try
{
var id = new CommandID(GuidList.guidMenuAndCommandsCmdSet, _slots[index]);
var command = new DynamicScriptCommand(id, file, GetCurrentClassFileName, OutputCommandString) { Visible = true };
mcs.AddCommand(command);
}
catch (Exception exception)
{
OutputCommandString(string.Format("Can't add t4 file {0}. Exception: {1}", file, exception.GetType()));
}
}
Таким образом в Меню при каждом запуске вставляется то количество скриптов, которое находится в папке PackageEnvironment.ScriptDirectoryFullPath (вплоть до N) и кнопка, которая эту папку открывает.
Скрипты считываются в realTime поэтому их можно править и тут же применять.
Template представляет из себя:
заглушку, в которой заранее проставлены все поддерживаемые параметры, которые может отыскать плагин в студии на текущем открытом файле и предоставить их в качестве параметров на вход в скрипт (для partial классов.
Весь output от трансформации (предупреждения и ошибки) перенаправляется в студийный output, а сам результат трансформации — в буфер обмена. На самом деле возможности позволяют добавлять их и в проект, но для абстрактной задачи преобразования это может быть неправомерно, хотя просто кидать в отдельные файлы, возможно стоит. Ну а буфер, мое личное мнение, просто нейтральная территория.
Таким образом мы избавились от прямого referense на EnvDTE внутри проекта, прицепив сценарные скрипты к IDE и обеспечили минимально-удобный интерфейс взаимодействия.
Часть 3. Валидация результата.
Надо понимать, что смысл этого расширения — это сделать не работу, а болванку для работы. Качество этой болванки может сильно отличатся в зависимости от задачи и скрипта.
Скрипты, со временем, будут притачиваться к задаче, улучшая вывод, вплоть до компилирующегося с ходу кода. Но минимальное условие употребимости — это хотя бы экономия во времени. Т.е. затраты написать самому без T4, должны быть больше, чем применить и пр иточить. Ну а плагин — лишь небольшой помощник в этом деле, который немного уменьшает «цену» T4.
Исходники на Github:
CodeGenerationExtention
Полезные ссылки:
Declarative Codesnippet Automation with T4 Templates
Project Metadata Generation using T4
CopyType* — фича ReSharper, Просто создает дубликат типа.
Автор: Arheus