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

в 14:48, , рубрики: .net, github, mvvm, Visual Studio, visual studio 2013

 Цель статьи: дать пару идей для автоматизации, а может даже и рабочий инструмент для создание 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) и скрипт, в примитиве представляющий собой следующее:

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

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

 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 представляет из себя:

T4 в помощь на примере MVVM - 3
заглушку, в которой заранее проставлены все поддерживаемые параметры, которые может отыскать плагин в студии на текущем открытом файле и предоставить их в качестве параметров на вход в скрипт (для 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js