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) і скрипт, примітив представляє собою наступне:



 Просто, але є декілька "поганих" моментів у використанні.

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

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, Просто створює дублікат типу.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.