My pages

by Sergey Vinyarsky

Пакеты расширений в Visual Studio

При разработке VSBookmarks мне пришлось столкнуться с острым недостатком внятной документации по внутренностям Visual Studio. На первый взгляд полно всего, начиная с MSDN и заканчивая многочисленными книгами, однако чуть копнешь глубже и простые примеры, приводимые в MSDN, становятся совершенно неприменимы. Особенно забавно наблюдать, как куски кода переползают из книги в книгу, меняя на ходу лишь названия переменных. Поэтому я решил систематизировать здесь те вещи, до которых мне удалось докопаться. Возможно кому-то эта информация поможет сэкономить немного времени. Если вы вдруг увидели у меня ошибку, сообщите, пожалуйста, на Sergey[at]Vinyarsky.ru. Начнем :)

Прежде всего надо понимать, что Visual Studio - продукт с очень богатой историей и как следствие сложной технической организацией. В настоящее время в нем сосуществуют на равных правах COM и .NET. Соответственно существуют два независимых API и мы вольны их по-всякому комбинировать. Плюс к этому расширения могут быть на управляемом или неуправляемом коде (managed or unmanaged code). Microsoft рекомендует писать “управляемые” расширения. Проще говоря, мейнстрим писательства расширений - это сборки .NET написанные на C#.

Существует несколько форматов расширений: Add-in, Visual Studio isolated shell application, VSPackage и MEF-компонент. Add-In декларируется компанией Microsoft как устаревший в VS2013, не будем его рассматривать. Visual Studio isolated shell application пока тоже опустим.

VSPackage - основной механизм расширительства. К слову сказать, в самой студии поддержка языков программирования реализована как расширения VSPackage. Пакет VSPackage - это dll-файл, содержащий COM-сервер с определенными интерфейсами и зарегистрированный в реестре как расширение студии. При разработке на неуправляемом коде нам необходимо аккуратно реализовать необходимые интерфейсы, однако для управляемого кода все становится значительно проще: компания Microsoft разработала ряд классов, которые реализуют все необходимые интерфейсы и нам достаточно лишь отнаследоваться от соответствующих классов. Получившаяся сборка загружается как COM-объект через COM-interop.

Прежде всего необходимо установить Visual Studio SDK.

Теперь давайте сделаем простой пакет, который добавит пункт меню и будет на него как-то реагировать. Самый простой путь - это воспользоваться мастером, который все сделает за нас:

  • Создайте новый проект Visual Studio (File/New/Project);
  • Выберите в ветке Other project types/Extensibility пункт Visual Studio Package;
  • В мастере на странице “Select a programming language” выберите Visual C#;
  • На странице “Basic VSPackage information” укажите наименование пакета VSPackage1;
  • На странице “Select additional functionality” выберите Menu Command;
  • Остальные опции оставьте как есть.

Все, пакет готов, можно запускать :) Создается экспериментальный экземпляр студии, в котором уже зарегистрировано наше расширение. В основной вашей студии этого расширения не будет, что позволяет отлаживаться без страха попортить сам инструмент разработки. В меню Tools появился пункт My command name, при клике на который появляется message box.

В созданном проекте куча файлов, однако важными для нас являются три из них: source.extension.vsixmanifest, VSPackage1.vsct и VSPackage1Package.cs.

source.extension.vsixmanifest - это VSIX-манифест. Созданные пакеты распространяются в виде VSIX-файла. Этот файл содержит бинарники и информацию о содержимом. Подробнее можно посмотреть здесь. При компиляции пакета формируется готовый vsix-файл, который вы можете распространять. Для установки расширения кликаем по файлу и запускается спец. установщик. Заполняйте манифест на ваше усмотрение, однако обязательно заполните параметр License, поскольку VSIX-installer отказывается ставить пакеты без лицензии. Если предполагается распространять расширение через Visual Studio Gallery, укажите Preview image. Размер изображения 200х200 точек. В разделе Install Targets перечисляются продукты, для которых предназначено расширение. По умолчанию указан только один продукт - тот, в котором вы создали проект. В моем случае это Microsoft.VisualStudio.Pro 12.0, то есть профессиональная версия VS2013.

VSPackage1.vsct - содержит список визуальных элементов (команд), которые будут добавлены в студию при старте расширения (VSCT - Visual Studio Command Table). Обычно это меню и команды в них. Файл содержит XML-данные и компилируется при сборке в бинарный CTO-файл (Command Table Output), который включается в VSPackage.resx. Здесь кроется небольшая засада: дело в том, что простое включение в проект файла с расширением vsct не приведет к желаемому результату, нам надо специально указать, что файл нужно скомпилировать особым образом и включить в ресурсы бинарника. Это невозможно сделать в UI студии, необходимо вручную отредактировать csproj-файл. Включите в него две секции:

и

В нашем случае это не требуется, поскольку мастер создания проекта все сделал за нас. Имейте в виду, что файл ресурса в любом пакете расширения обязан называться VSPackage.resx.

Типичный VSCT-файл выглядит следующим образом:

Я не привожу здесь полного описания синтаксиса, мы его рассмотрим в следующий раз.

VSPackage1Package.cs - содержит главный класс нашего пакета. Класс наследуется от Microsoft.VisualStudio.Shell.Package, в котором реализована вся функциональность, требуемая для загрузки пакета как расширения студии. Дополнительные сведения загрузчику передаются через атрибуты класса:

    // Заносит в манифест пакета PkgDef, что именно этот класс представляет пакет
    [PackageRegistration(UseManagedResourcesOnly = true)]

    // Добавляет в окно About сведения о пакете (110, 112 и 400 - номера ресурсов) 
    [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]

    // Сообщает о наличии в ресурсе скомпилированного VSCT-файла 
    [ProvideMenuResource("Menus.ctmenu", 1)]

    // Идентификатор пакета. Важно его не менять, т.к. именно по идентификаторам
    // студия различает пакеты.
    // Если выпустить обновление с другим гуидом, студия поставит его рядом,
    // а не вместо предыдущей версии.
    [Guid(GuidList.guidVSPackage1PkgString)]   

Далее создается сама команда и callback-функция, которая будет вызвана при клике по команде. Код говорит сам за себя, из интересного только стоит обратить внимание на строку

    OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;

Архитектурно студия представляет собой множество пакетов, которые предоставляют сервисы. Плюс ядро само предоставляет ряд сервисов. Основной способ взаимодействия расширения с внешним миром, это использование сервисов. Для этого в классе Package имеется метод GetService. В этот метод передается желаемый тип сервиса. Результат метода необходимо приводить к запрашиваемому типу.

Наконец мы написали расширение. Что с ним делать дальше? Пакеты могут распространяться как VSIX-файлы, которые пользователь устанавливает вручную, либо посредством бесплатного размещения в галерее. Оба эти метода поддерживают корректную процедуру обновление ранее установленных расширений, главное, как я писал выше, не менять гуид.

Все, пробежались по верхам. Пока за кадром остались MEF-компоненты, плюс хочется по-подробней остановиться на VSCT-файлах. Еще есть очень интересная и мутная тема взаимодействия с редактором кода и работа с клавиатурой. Постараюсь не затягивать с продолжением. Если у вас возникли вопросы и замечания, пишите мне на Sergey[at]Vinyarsky.ru (ага, согласен, что неудобно, надо бы приделать комментарии).

Спасибо, что дочитали до конца :)