Наконец, мы подошли к кульминации серии статей по автоматизации создания однотипных классов, проектов и даже целых архитектур. И расскажу я здесь про азы создания пользовательских расширений, про подключение с помощью расширений шаблонов элементов и проектов, про создание Wizard для наших шаблонов проектов, и про то, как удобнее это все использовать.

Создание пользовательских расширений

Добавляем новый проект. В окне выбора шаблонов ищем по фразе "vsix".

Тут нас интересуют 2 верхних шаблона. Создадим пустой.

Так же создадим проект типа шаблон проекта и проект типа шаблон элемента, пользуясь инструкцией из прошлой статьи. Ничего кастомизировать не будем, просто добавим их в наше расширение. Для этого откроем выделенный на скрине файл.

Этот файл откроется вот в таком виде

Погружать вас в подробности, которые вам скорее всего не понадобятся, я не буду, для этого есть официальная документация от Microsoft, где подробно рассказано что и как, поэтому просто объясню, как подключить сюда наши шаблоны.

Переходим в раздел Assets и нажимаем New.

В качестве типа проекта выбираем Microsoft.VisualStudio.ProjectTemplate (затем все то же самое проделаем для ItemTemplate). В качестве Source выберем A project in current solution. Можно было выбрать файл на диске и указать путь до скомпилированного zip файла с шаблоном, но не уверен что это будет удобно. После того, как мы выбрали Source, в окне появляются дополнительные элементы управления.

В Project выбираем проект нашего шаблона проектов и нажимаем ОК.

Теперь проделаем то же самое для ItemTemplate. Выберем наш проект с расширением как запускаемый и запустим. При этом у нас откроется новый экземпляр студии с единственным расширением - нашим. Для проверки того, что у нас все получилось, мы можем создать проект по нашему шаблону, затем создать в нем класс по нашему шаблону. С этим все просто, проблем возникнуть не должно. Теперь добавим визард (есть русское слово "мастер", но оно не очень популярно и его могут не все понять, а переключать раскладку на английский каждый раз не хочется, поэтому я буду просто писать по-русски "визард") для настройки нашего проекта при создании.

Wizard для шаблонов

Wizard - это не просто какое то окошечко. Это целое приложение! Точнее библиотека. И точкой входа для шаблона является реализация интерфейса Microsoft.VisualStudio.TemplateWizard.IWizard.

Я бы хотел на этом этапе абстрагироваться от типа приложения, но для реализации IWizard нам нужно его создать. Поэтому об этом расскажу сразу. Визард наш должен быть запускаемым приложением или библиотекой .Net Framework удобной для вас версии. А там, вы можете хоть консольное приложение создать. Только консоль самому нужно будет открыть (кодом). Такие выкрутасы вы можете попробовать сами, если захотите, а я создал WPF библиотеку. Код окна и ViewModel я тут представлять не буду, потому что это не мануал по WPF + MVVM, все примеры пробные и не красивые. Да и вообще вам может быть ближе Windows Form. Поэтому покажу только вид окна и реализацию интерфейса визарда.

Но для начала нам нужно подключить к проекту необходимые зависимости.

  • Microsoft.VisualStudio.TemplateWizardInterface

  • envdte

Этого будет достаточно. Для расширенного взаимодействия со средой разработки и создаваемыми объектами проекта и его наполнения могут потребоваться и другие референсы, но это все вы сами нагуглите, потому что я ведь не знаю, чего вы захотите сделать.

А вот код визарда.

using System.Collections.Generic;
using EnvDTE;
using Microsoft.VisualStudio.TemplateWizard;

namespace WpfControlLibrary1
{
    public class MyWizard : IWizard
    {
        // При создании элементов проекта (классов и тд), их можно 
        // сразу открыть для редактирования. Этот метод сработает
        // перед открытием.
        public void BeforeOpeningFile(ProjectItem projectItem)
        {
            
        }

        // Окончание генерации проекта. В этот момент все, что
        // требовалось по шаблону проекта уже выполнено и у нас
        // есть готовый объект проекта. Тут мы можем нагенерить
        // еще элементов, используя настройки, заданные в нашем 
        // визарде, например.
        public void ProjectFinishedGenerating(Project project)
        {
        
        }

        // Этот метод сработает для каждого элемента проекта, указанного
        // в шаблоне каждого проекта.
        public void ProjectItemFinishedGenerating(ProjectItem projectItem)
        {
            
        }

        // Этот метод сработает по окончании работы визарда.
        public void RunFinished()
        {
            
        }

        // Этот метод сработает на старте визарда. 
        // Визард запускается в тот момент, когда вы выбрали шаблон
        // поректа, затем задали имя проекта и путь в 
        // стандартном диалоге студии.
        // тут мы и будем открывать наше окно с настройками.
        // Параметры разбирать подробно не буду, скажу только,
        // что replacementsDictionary содержит набор парамтеров шаблона.
        // Сюда можно добавить свои параметры для простых кейсов
        // (замена в шаблоне). Именно это я и делаю.
        public void RunStarted(object automationObject, 
            Dictionary<string, string> replacementsDictionary, 
            WizardRunKind runKind, object[] customParams)
        {
            var vm = new Window1Vm
            {
                FirstClassName = "MyClassName",
                FirstPropertyName = "MyPropertyName"
            };
            var window = new Window1(vm);
            var res = window.ShowDialog();

            if (res == false)
                throw new WizardCancelledException("Отмена создания проекта");

            // В итоге мы в визарде задаем имя нашего класса и имя свойства.
            // В шаблоне я явно использовал эти переменные.
            // Не думаю, что это может быть практически полезно,
            // но это ведь просто пример.
            replacementsDictionary["$firstclassname$"] = vm.FirstClassName;
            replacementsDictionary["$firstpropertyname$"] = vm.FirstPropertyName;
        }

        // Здесь мы можем повлиять на создание элемента проекта
        // по пути файла с этим элементом.
        public bool ShouldAddProjectItem(string filePath)
        {
            return true;
        }
    }
}

Теперь о том, как этот визард прицепить к шаблону. В официальной документации есть подробная информация об этом, вот только Microsoft предлагают нашу сборку с визардом подписать и зарегистрировать в GAC. Это не сильно большая проблема, если вы делаете расширение для личного пользования, но вам надо его еще и распространить для команды. Можно, конечно, написать cmd/ps скрипт для всех этих процедур, но мне не нравится такой подход. Хотелось бы, чтобы расширение само решило эту задачу, ведь оно не имеет смысла без этого визарда. В документации я так же нашел различные способы развернуть расширение. Например, MSI умеет регистрировать сборки в GAC, но тогда и удалить расширение вы сможете только через анинсталлер, а мне хочется иметь возможность управлять расширениями через оснастку Visual Studio. А это значит, что мой путь VSIX.

Если пошариться по интернетам, можно отыскать различные статьи, которые так же работают с регистрацией через GAC и ни про какое распространение для команды нет и речи. И вот я в 3й раз открыл уже ранее открытое обсуждение на SO. Оказывается, ответ, который мне подошел, там ответом не является и вообще имеет 0 голосов. Там разработчик предлагает добавить сборку в Assets (так же как мы делали с шаблонами, только выбрать в качестве типа Assembly). Ну это я и так нашел методом тыка. А вот как подключить ее к шаблону?

  <WizardExtension>    
    <Assembly>WpfControlLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</Assembly>
    <FullClassName>WpfControlLibrary1.MyWizard</FullClassName>
  </WizardExtension>

Это нужно добавить в шаблон проекта. Я пробовал использовать короткое имя сборки, но это не сработало. Зато получилось с PublicKeyToken=null. Не очень безопасно, но тут я думаю, что вы доверяете своей команде, а если злоумышленник проникнет в вашу сеть и получит доступ к вашим серверам и компьютерам, подпись не спасет. Главное, не распространять это расширение в официальном магазине Microsoft.

Выглядит, кстати, не очень удобно. Получается, что шаблон проекта зависит от визарда, который добавляется в расширение. То есть шаблон зависит от окружения и не является самостоятельным. Эту проблему можно решить, если шаблон с визардом сделать отдельным расширением, добавить в него визард и подключить его к основному расширению. То есть одно расширение будет устанавливать одно или несколько других. Как это сделать? Для этого в дизайнере манифеста расширения в разделе Dependencies нужно добавить зависимое расширение (в виде файла или проекта).

Тогда, при инсталляции основного расширения, установятся и вложенные.

Что еще мы можем в расширении?

Много всего. У нас есть доступ к интерфейсу IDE. Мы можем управлять открытыми документами, решениями, проектами, редактором кода, основным меню, различными контекстными меню. Я даже не буду все перечислять. Но что еще можно было бы сделать, чтобы работать с конкретным проектом стало удобнее?

Мы уже добавили шаблоны элементов. Однако доступ к ним все еще издалека. Можно добавить команду в контекстное меню проекта для создания элемента из нашего шаблона. Можно даже завязать на эту команду горячую клавишу. Можно сделать, чтобы команда добавлялась в меню для проектов по каким-либо критериям.

Так же мы можем поступить и с шаблонами проектов.

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

Настройка HTTPS по просьбе в комментариях

В одной из статей цикла меня спросили про настройку https для проекта. Очевидно, что поддержка https это не задача самого приложения, но задача настройки среды, где это приложение развернуто. А для локального запуска нужно отредактировать файл launchSettings.json. Или же добавить готовый файл в шаблон. В файле этом должен быть вот такой блок

  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:25440",
      "sslPort": 0
    }
  }

Здесь нас интересует sslPort. Если он задан, то при локальном запуске приложение будет доступно по защищенному протоколу по указанному порту. Ну и, конечно, должен быть профиль для запуска через IIS Express.

  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
Вот полный код файла
{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:25440",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "WebApplication1": {
      "commandName": "Project",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "dotnetRunMessages": "true",
      "applicationUrl": "https://localhost:5001;http://localhost:5000"
    }
  }
}

Заключение

Время разработчиков стоит довольно дорого. Поэтому я пошел рабо...

Комментарии (6)


  1. pankraty
    03.09.2021 13:20

    У разработки кастомных расширений под проект есть пара недостатков. Во-первых, вся команда привязывается к одной IDE и даже к одной версии IDE. А значит, если кто-то предпочитает Rider, скажем, то ему придется подстраиваться под остальных. Равно как любителям попробовать новейшие превью версии придется ждать обновления расширения под новую студию. Плюс, со временем к проекту могут подключаться внешние подрядчики, у которых может не оказаться лицензии на нужную версию той же студии.

    Кроме того, есть тенденция, что с уходом основного драйвера, развивавшего расширение, его поддержка прекращается, заморозившись на какой-то версии. И все это работает до первого ломающего изменения, после чего перед организацией встает выбор: или оттягивать обновление до последнего (и продолжать работать на VS10 в 2021) или всей команде перестраивать процессы, привычно автоматизированные в расширении.

    Не утверждаю, что так бывает всегда, но это то, с чем сам сталкивался.


    1. iRumba Автор
      03.09.2021 16:05

      Если 10 человек работают со студией, а пара человек нет, то все норм! Даже если пополам, тимлид же знает, кто быстрее создаст новый проект в решении или добавит новых контроллеров. Следовательно, задачи уйдут кому нужно.

      Расширения со старых версий студии чаще всего отлично перекочуют в новую версию, если, конечно, не будет меняться версия платформы и языка самого проекта.

      Если работает 5 человек в студии и 5 в райдере и проект долгий, можно создать расширения для обоих IDE. А можно вообще основать всю логику на шаблонах для дотнета, а в расширение встроить необходимую инфраструктуру для включения их в IDE. Можно завязаться на генераторы Т4. Да хоть на скрипты PS. Тогда получится своего рода кроссплатформенность. Как Xamarin. У тебя есть корневой функционал, но UI ты делаешь для iOS и для андрюши отдельно. Да, будет сложнее поддерживать, но о целесообразности нужно думать в каждом конкретном случае. Я же предложил подход, чтобы было вообще о чем думать. А примеры затачивал под студию, потому что большинство известных мне дотнетчиков работают именно в этой IDE.


      1. pankraty
        03.09.2021 16:36

        Да-да, я согласен. Писал не с целью критики подхода, а чтобы дополнить статью соображениями о возможных подводных камнях на периоде от 5 лет и больше.

        Плюс, при команде условно в 10 человек проблем будет меньше. Они начнут проявляться на уровне 30-50 человек, т.к. для большинства инструменты по автоматизации будут являться черным ящиком, который либо работает, либо нет.


  1. Refridgerator
    03.09.2021 13:58

    Доводилось делать vsix для раскрашивания и сворачивания кода на ассемблере. Документацию не читал, проект с нуля не создавал — а просто скачал примеры и правил их до тех пор, пока не получился нужный результат.


    1. iRumba Автор
      03.09.2021 15:57

      Да, я всегда так делаю. С кодом из SO )


  1. anonymous
    00.00.0000 00:00