T4 в помощь на примере MVVM

 Цель статьи: дать пару идей для автоматизации, а может даже и рабочий инструмент для создание T4-болванок под решения типовых задач, производимых с классами/интерфейсами в работе. Немного конкретики: есть совсем простые, хорошо алгоритмизированные, повседневные задачи —  из DTO-класса написать Model, из Model —  ViewModel, типовые Тесты или трансформация объекта в Json. Задачи, которые отлично ложатся на механизмы T4, но которые долго или неудобно применять. Разработчик обычно стоит на распутье: выполнить задачу или написать скрипт, который, может быть, поможет ее выполнить. Иногда скрипт кажется слишком сложным, иногда человека останавливает мысль о том, что скрипт по сути —  не функциональная часть проекта, а скрипта-на-один-раз. И сверяя сложность линейной задачи с невыясненной —  разработчик часто отказывается от второго пути. Но чем больше у него опыта, тем меньше ему нравится линейный труд. И встает вопрос — как донести возможности T4 для всей команды, не заставляя ее вникать, а предоставляя возможность вникнуть. При том, поддержка должна быть минимальна. Обычные T4 скрипты нуждается в поддержке (что бы быть употребленным еще раз, должен таскаться вместе с разработчиком по проектам, в какой-то момент должен быть «подпиленным» под определенную задачу). И такого хочется избежать. В идеале же, я полагаю, процесс должен состоять из нажатия волшебной кнопки, которая генерирует нужную болванку автоматически, в зависимости от требуемого сценария. И ее уже разработчик сможет быстро доработать до практической применимости в разрезе конкретной ситуации. Т.е. на самом деле я предлагаю поработать над недостатками особенностей использования скриптов кодегенерации. Качество и время как главные критерии, потому все «волшебство» можно разделить на 3 составляющих:

Шаблонная генерация отдельного сценария. Простая интеграция в процесс разработки. Валидация результата.  Теперь по порядку: Часть 1. T4 генерация мечты. Проще всего будет показать весь процесс на определенной задаче. MVVM, путешествие сущности из объекта сервиса до объекта верхних уровней.

 Часто происходит так, большая часть DTO-объекта попадает в модель с минимально простыми изменениями. Объект транспортного уровня не несет в себе никакой логики, только данные — и модель в минимальном исполнении должна минимум копировать часть его свойств, налагая права доступа к ним. Если применить это к слоям MVVM, выражается это в трансформации:

image

Итого 2 трансформации. На выходе первого преобразования решение должно дорабатываться человеком, привнося логику. Затем еще одна трансформация, создавая уже более высокую ступеньку. Думаю, схожую функциональность может обеспечить и CopyType* + AutoMapper. Так же, могут помочь и аспекты (для реализации стандартных реакций). И в простых случаях лучше использовать именно их. Но, мое личное мнение, если mapping перестает быть тривиальным, или отладка превращается в Code Hell — надо упрощать объекты, работая в первую очередь над повышением читаемости кода и механизмы, скрывающие под капотом важную функциональность тут плохие помощники. Повторюсь, это имхо.

 Для таких нехитрых преобразований понадобится помощь T4 toolbox​ (спасибо Oleg V. Sych) и скрипт, в примитиве представляющий собой следующее:

3b2e4811fe47417992647eb50aa416d7.png

 Просто, но есть несколько «плохих» моментов в использовании.

 1. Что бы использовать тип CodeElement/CodeClass/CodeProperty, в месте использования надо оставлять ссылку на библиотеку ​EnvDTE​​ что не сочетается с потребностями проекта (библиотека относится к среде VisualStudio, будет очень странным добавлять и подчищать ссылки за собой). 2. Источник для преобразования этим скриптом должен быть указан явно. 3. В общем случае не решается вопрос с доступностью полей в сгенерированном коде. Но, по правде сказать, этот вопрос я так и не решил, отдав его на откуп разработчиков, рассудив что стереть setter-ы проще, чем на каждый тип делать схему доступности. Это сильно экономит время, упрощает скрипт, но качество «выхода» снижает.

 Первые два недостатка мы можем исправить, перейдя ко второй части:

Часть 2. Интеграция в процесс разработки.  Интегрироваться в процесс разработки безболезненно можно с помощью нескольких инструментов. Было бы очень приятно использовать что-то вроде Resharper-овского контекстного меню, которое на конкретном классе дает выпадающий список из возможных скриптов, но я не нашел способа создать такой плагин, буду раз, если кто подскажет. Однако, плагин к самой студии создать вполне реально.

 Для этого понадобится установка Visual Studio SDK и немного времени, для того, что бы разобраться как встраиваться в IDE от Microsoft.Wizard делает большую часть работы, потому освещать этот процесс не стану, но остановлюсь на 2 важных файлах: .vsct​ и ​MenuCommandsPackage.

 В первой надо описать плоскую структуру построения меню в формате xml, где каждая кнопка представлена в виде:

 Т.е. в .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 в txt формате и один .tt, который можно свободно править в любой момент и прогонять с любым содержимым.

Template представляет из себя: 308c39b799ac4dc390a7a6f68722a569.pngзаглушку, в которой заранее проставлены все поддерживаемые параметры, которые может отыскать плагин в студии на текущем открытом файле и предоставить их в качестве параметров на вход в скрипт (для partial классов. Весь output от трансформации (предупреждения и ошибки) перенаправляется в студийный output, а сам результат трансформации — в буфер обмена. На самом деле возможности позволяют добавлять их и в проект, но для абстрактной задачи преобразования это может быть неправомерно, хотя просто кидать в отдельные файлы, возможно стоит. Ну, а буфер, мое личное мнение, просто нейтральная территория.

 Таким образом мы избавились от прямого referense на EnvDTE внутри проекта, прицепив сценарные скрипты к IDE и обеспечили минимально-удобный интерфейс взаимодействия.

Часть 3. Валидация результата.  Надо понимать, что смысл этого расширения — это сделать не работу, а болванку для работы. Качество этой болванки может сильно отличатся в зависимости от задачи и скрипта.Скрипты, со временем, будут притачиваться к задаче, улучшая вывод, вплоть до компилирующегося с ходу кода. Но минимальное условие употребимости — это хотя бы экономия во времени. Т.е. затраты написать самому без T4, должны быть больше, чем применить и пр иточить. Ну, а плагин — лишь небольшой помощник в этом деле, который немного уменьшает «цену» T4.

 Исходники на Github: CodeGenerationExtention Полезные ссылки: ​Declarative Codesnippet Automation with T4 TemplatesProject Metadata Generation using T4​

CopyType* — фича ReSharper, Просто создает дубликат типа.

© Habrahabr.ru