Руководство по разработке модулей расширений на C# для Visual Studio 2005-2012 и Atmel Studio




Аннотация

Около года назад мы опубликовали в блоге цикл статей о разработке плагинов для Visual Studio на языке C#. Сейчас мы переработали эти материалы, добавили новые разделы и предлагаем вашему вниманию обновленный вариант руководства.

Создание модулей расширения (или плагинов) для среды разработки Microsoft Visual Studio поначалу может показаться очень простым. Ведь есть прекрасная документация в MSDN, статьи, примеры и много дополнительных материалов. Но может показаться и сложным, когда некоторые действия будут давать не тот результат, который ожидается. И хотя такое поведение нередко оказывается характерно для любой программистской задачи, все-таки тема разработки плагинов не полностью раскрыта.

Мы занимаемся разработкой статического анализатора кода PVS-Studio. И хотя сам инструмент предназначен для программистов на C++, немалая его часть написана на C#. Когда мы начинали разрабатывать наш плагин, в мире Visual Studio самой новой и современной считалась версия Visual Studio 2005. И хотя сейчас, когда уже вышла Visual Studio 2012, некоторые могут сказать, что Visual Studio 2005 не совсем актуальна, мы до сих пор поддерживаем эту версию в своем инструменте. За то время что мы поддерживали разные версии Visual Studio и использовали разные возможности среды, у нас накопилось большое количество практических знаний о том, как же правильно (а особенно неправильно!) создавать плагины. Держать в себе эти знания больше не было никаких сил. Поэтому мы решили оформить их и опубликовать. Ведь некоторые решения, которые сейчас кажутся очевидными, были найдены только несколько лет спустя. А те, проблемы, которые уже давно решены, до сих пор могут мучать некоторых разработчиков плагинов.

Будут рассмотрены следующие вопросы:

  • базовая информация по созданию и отладке MSVS плагинов, а также поддержка данных проектов расширения в единой кодовой базе для нескольких версий Visual Studio;
  • обзор объектной модели автоматизации и классов MPF (Managed Package Framework);
  • расширения интерфейса среды разработки с использованием API объектной модели автоматизации (EnvDTE) и классов MPF (Managed Package Framework) пользовательскими меню, панелями инструментов, инструментальными окнами и диалогами настроек;
  • обзор проектной модели Visual Studio, взаимодействие с пользовательскими проектными моделями на примере реализованной в виде Visual Studio Isolated Shell среды Atmel Studio
  • сбор всех необходимых данных, таких как параметры и настройки компиляции разных конфигураций и платформ, с помощью проектной модели Visual C++ для работы с внешним препроцессором/компилятором;

Более детальное и полное описание затронутых в статье тем доступно по приведённым в конце каждого раздела ссылкам на официальные материалы библиотеки MSDN и другие сторонние ресурсы.

Рассматриваться будет только разработка подключаемых модулей для Visual Studio 2005 и выше. Это ограничение обусловлено тем, что PVS-Studio поддерживает только системы с VS2005 и выше. Такое ограничение при разработке PVS-Studio вызвано появлением в среде Visual Studio 2005 новой модели API, которая не совместима с предыдущими версиями API расширения среды.

Создание, отладка и развертывание пакетов расширения сред Microsoft Visual Studio 2005/2008/2010/2012

В данном разделе будет произведён обзор различных методов расширения функциональности среды Visual Studio. Подробно будет рассмотрено создание модулей расширения вида Visual Studio Extension Package (пакет расширения Visual Studio), их отладка, регистрация и развёртывание на машине конечного пользователя.

Создание и отладка VSPackage модулей расширения Visual Studio и Visual Studio Isolated Shell

Существует множество способов для расширения функционала Microsoft Visual Studio. На самом базовом уровне можно автоматизировать простые рутинные действия с помощью макросов. Для программной автоматизации простых действий с UI объектами среды, манипуляций пунктами в меню и т.п. можно использовать подключаемый модуль (Add-In).

Расширение встроенных редакторов среды возможно через MEF (Managed Extensibility Framework) компоненты (начиная с версии MSVS 2010). Для интеграции в Visual Studio крупных независимых компонентов лучше всего подходят расширения вида Extension Package (пакеты расширения, также известные как VSPackage). При этом VSPackage позволяют сочетать в себе автоматизацию управления компонентами IDE через объектную модель автоматизации с расширением среды через MEF (Managed Extensibility Framework) и Managed Package Framework классы (таких, как Package). На самом деле, тогда как сама Visual Studio предоставляет лишь общие интерфейсные компоненты и службы, такие стандартные модули, как например Visual C++ или Visual C#, реализованы как раз в виде расширений среды.

Первые версии плагина PVS-Studio (точнее 1.XX и 2.XX, когда наш продукт еще назывался Viva64), мы выпускали как Add-In. С версии PVS-Studio 3.00 мы переделали его на VSPackage. Причина перехода – нам стало "тесновато" в Add-In и было неудобно отлаживаться. Кроме того, хотелось иметь свой значок на экранной заставке Visual Studio!

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

Помимо расширения непосредственно самой среды Visual Studio, VSPackage модули можно использовать и для добавления функционала в изолированные\интегрированные оболочки Visual Studio (Visual Studio Isolated\Integrated Shell). Изолированная\интегрированная оболочка Visual Studio позволяет любому стороннему разработчику пере-использовать стандартные интерфейсные компоненты и службы Visual Studio (редактор кода, система автодополнения и т.п.), добавив в среду поддержку своих собственных проектной модели и\или компиляторов. Такой дистрибутив не будет содержать проприетарных языковых компонентов Microsoft (Visual Basic, Visual C++ и т.п.), и может быть установлен конечным пользователем даже на систему без предустановленной версии Visual Studio IDE.

Изолированная оболочка Visual Studio будет оставаться обособленной после установки даже на системе с предустановленной Visual Studio, а интегрированная оболочка будет объединена с предустановленной средой. В случае если разработчик изолированной\интегрированной оболочки расширит модель автоматизации Visual Studio, добавив в неё интерфейсы для своих специфических компонентов, разработчик VSPackage плагина будет иметь доступ и к этим интерфейсам. В качестве примера Visual Studio Isolated Shell можно привести среду для создания embedded систем Atmel Studio. Atmel Studio использует собственную проектную модель, являющуюся реализацией стандартной проектной модели Visual Studio для MSBuild, и вариант компилятора gcc.

Проекты подключаемых модулей VSPackage. Создание пакета расширения

Рассмотрим создание подключаемого модуля вида Visual Studio Package (VSPackage, пакет расширения). В отличие от подключаемых модулей (Add-In), разработка пакета расширения среды потребует установки Microsoft Visual Studio SDK для целевой версии среды разработки. То есть для разработки пакета расширения под каждую версию Visual Studio потребует установки отдельного SDK. При создании пакета расширения для Visual Studio Isolated\Integrated Shell потребуется SDK для той версии Visual Studio, на которой данная оболочка основана.

В дальнейшем мы будем рассматривать разработку расширений для версий среды 2005, 2008, 2010 и 2012 и Visual Studio 2010 Isolated Shell (на примере Atmel Studio). Установка Visual Studio SDK добавляет в стандартные шаблоны среды проект типа Visual Studio Package (пункт Other Project Types -> Extensibility). Данный шаблон сгенерирует простейший MSBuild проект для модуля расширения, позволяя задать язык разработки и заглушки для нескольких типовых компонентов (пункт меню, редактор, пользовательское окно).

Мы будет использовать C# проект (csproj) VSPackage, представляющий собой MSBuild проект динамически подключаемой библиотеки (dll). В нашем случае это managed assembly, содержащий также несколько специфичных для пакетов расширения Visual Studio сборочных XML узлов, таких как VCST компилятор и IncludeInVSIX для последних версий Visual Studio.

Основной класс пакета-расширения Visual Studio должен быть унаследован от класса Microsoft.VisualStudio.Shell.Package. Этот базовый класс предоставляет managed-обёртки для интерфейсов взаимодействия с IDE, необходимых полноценному пакету расширения Visual Studio.

public sealed class MyPackage: Package
{
  public MyPackage ()
  {}
  ...
}

Класс Package предоставляет возможность переопределения базового метода Initialize. Метод Initialize получает управление в момент инициализации пакета расширения для текущей сессии IDE.

protected override void Initialize()
{
  base.Initialize();

  ...
}

Инициализация модуля происходит при первом обращении к нему, а также может быть вызвана автоматически, например, при запуске IDE, при вхождении пользователя в заданный UI контекст (например, открытие проекта) и т.п.

Вообще очень важно понимать, как стартует и как завершается модуль расширения. Ведь может оказаться, что разработчик пытается использовать какой-то функционал Visual Studio, который в данный момент использовать нельзя. При разработке PVS-Studio у нас бывали ситуации, когда среда "била нас по рукам" за непонимание того, что при завершении Visual Studio нельзя "в лоб" показать message box с вопросом.

Отладка пакетов расширения. Experimental Instance

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

Надо отметить, что отлаживать VSPackage значительно удобнее, чем Add-In. Это послужило одной из причин для смены используемой в PVS-Studio модели работы с Add-In на VSPackage.

Для решения перечисленных проблем при разработке и отладке пакетов VSPackage можно использовать экспериментальный экземпляр Visual Studio (experimental instance). Его можно запустить, добавив в строку аргументов запуска среды специальный параметр:

"C:\Program Files (x86)\Microsoft Visual Studio 10.0\
  Common7\IDE\devenv.exe" /RootSuffix Exp

Экспериментальный экземпляр среды использует отдельную независимую ветку в системном реестре (experimental hive) для сохранения регистрационной информации установленных компонентов и настроек среды. Любые изменения в настройках IDE, регистрация или модификация новых компонентов в ветке Experimental Hive никак не отразятся на том экземпляре среды, который используется непосредственно для разработки и отладки модуля (т.е. в основной базовой версии, запускаемой по умолчанию).

Visual Studio SDK предоставляет специальную утилиту для создания или очистки экспериментальных экземпляров — CreateExpInstance. Вызов CreateExpInstance для создания новой экспериментальной ветки выглядит следующим образом:

CreateExpInstance.exe /Reset /VSInstance=10.0 /RootSuffix=PVSExp

Выполнение этой команды создаст новую экспериментальную ветку реестра с суффиксом PVSExp в имени для 10 версии IDE (Visual Studio 2010) с предварительным сбросом всех настроек среды до значений по умолчанию. Путь до новой ветки в системном реестре будет выглядеть так:

HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\10.0PVSExp

Хотя по умолчанию при отладке в шаблонном проекте VSPackage используется суффикс Exp (и соответствующая ему ветка реестра), ничто не мешает создавать и другие экспериментальные ветки, соответственно с другими суффиксами имён. Для запуска экземпляра среды в созданной нами ранее новой экспериментальной ветке (содержащей PVSExp в имени) нужно выполнить:

"C:\Program Files (x86)\Microsoft Visual Studio 10.0\
  Common7\IDE\devenv.exe" /RootSuffix PVSExp

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

После установки SDK в меню программ также будет добавлена ссылка, позволяющая сбросить настройки экспериментального экземпляра IDE до значений по умолчанию (например, Reset the Microsoft Visual Studio 2010 Experimental Instance).

При разработке расширения для Isolated Shell описанных выше проблем с "порчей" среды разработки не возникает, поэтому в использовании Experimental Instance необходимости нет. Но, в любом случае, чем быстрее вы разберетесь с тем, как работать с отладочным окружением, тем меньше у вас будет проблем с непониманием что, почему и как загружается при разработке плагина.

Регистрация и развёртывание пакетов расширения Visual Studio

Регистрация пакета расширения требует регистрации непосредственно самого пакета, а также всех интегрируемых им в IDE компонентов (например, пункты меню, страницы настроек, пользовательские окна и т.п.). Регистрация осуществляется через создание соответствующих компонентам записей в основной ветке системного реестра Visual Studio.

Вся информация о необходимых для регистрации компонентах записывается в специальный файл pkgdef во время сборки VSPackage проекта на основании специальных атрибутов основного класса модуля (подкласс Package). Файл pkgdef также можно сгенерировать вручную с помощью утилиты CreatePkgDef. Данная утилита собирает регистрационную информацию о модуле методом .NET рефлексии через специальные атрибуты подкласса package. Рассмотрим эти атрибуты.

Атрибут PackageRegistration сообщает регистрационной утилите о том, что данный класс является модулем-расширением Visual Studio. После его обнаружения будет произведён поиск дополнительных регистрационных атрибутов.

[PackageRegistration(UseManagedResourcesOnly = true)]

Атрибут Guid задаёт уникальный идентификатор модуля-расширения, который затем будет использован для создания регистрационной под-ветки в системном реестре, в ветке Visual Studio.

[Guid("a0fcf0f3-577e-4c47-9847-5f152c16c02c")]

Атрибут InstalledProductRegistration позволяет добавить информацию в Help -> About диалог и на splash экран загрузки среды Visual Studio.

[InstalledProductRegistration("#110", "#112", "1.0", 
  IconResourceID = 400)]

Атрибут ProvideAutoLoad позволяет назначить автоматическую инициализацию модуля на активацию заданного UI контекста среды. При вхождении пользователя в данный контекст модуль будет подгружен и инициализирован автоматически. Приведём пример назначения инициализации модуля на открытие solution файла.

[ProvideAutoLoad("D2567162-F94F-4091-8798-A096E61B8B50")]

Значения GUID идентификаторов для различных контекстов IDE можно посмотреть в классе Microsoft.VisualStudio.VSConstants.UICONTEXT.

Атрибут ProvideMenuResource задаёт ID ресурсов пользовательских команд и меню для их регистрации в IDE.

[ProvideMenuResource("Menus.ctmenu", 1)]

Атрибут DefaultRegistryRoot задаёт путь для записи регистрационной информации в системном реестре. Начиная с Visual Studio 2010 данный атрибут можно не использовать, а соответствующая ему регистрационная информация должна быть записана в манифесте VSIX контейнера. Пример использования атрибута для регистрации модуля в Visual Studio 2008:

[DefaultRegistryRoot("Software\\Microsoft\\VisualStudio\\9.0")]

Регистрация других пользовательских компонентов, таких как инструментальные и документные окна, редакторы, страницы настроек и т.п., также требует добавления соответствующих им атрибутов для пользовательского подкласса Package. Мы будем рассматривать такие атрибуты по мере рассмотрения самих компонентов.

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

[AttributeUsage(AttributeTargets.Class, Inherited = true,
  AllowMultiple = false)]
    public class CustomRegistrationAttribute : RegistrationAttribute
    {
    }

Атрибут-наследник RegistrationAttribute обязан будет переопределить методы Register и Unregister, которые используются для модификации регистрационной информации в системном реестре.

Для добавления регистрационной информации в реестр можно воспользоваться утилитой RegPkg, которая автоматически зарегистрирует все перечисленные в переданном ей pkgdef файле компоненты в заданную через аргумент /root ветку реестра. Так, например, вызов RegPkg автоматически прописывается в проектах Visual Studio для регистрации разрабатываемого модуля в экспериментальной ветке реестра для удобства его отладки. После добавления всей регистрационной информации в реестр нужно запустить среду Visual Studio (devenv.exe) с параметром /setup для регистрации новых компонентов уже в самой IDE.

Развертывание плагина на машине разработчика и на машине конечного пользователя. Package Load Key

Прежде чем приступить к описанию процедуры развертывания, запомните важное правило:

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

Сейчас, когда первые релизы PVS-Studio давно уже позади, у нас не бывает проблем с этим. Однако поначалу несколько неудачных версий попадали к пользователям.

Развёртывание плагина для сред Visual Studio 2005/2008 потребует запуска regpkg для pkgdef файла с указанием основной ветки реестра IDE либо добавления всех ключей из файла pkgdef в системный реестр вручную. Пример команды для автоматического добавления в реестр регистрационной информации из pkgdef файла (в одну строку):

RegPkg.exe /root:Software\Microsoft\VisualStudio\9.0Exp
  "/pkgdeffile:obj\Debug\PVS-Studio-vs2008.pkgdef"
  "C:\MyPackage\MyPackage.dll"

После добавления регистрационной информации в реестр необходимо запустить Visual Studio с параметром /setup, обычно это последний шаг процедуры инсталляции нового модуля.

Devenv.exe /setup

Запуск среды с данным ключом указывает Visual Studio на необходимость вычитывания метаданных о ресурсах пользовательских компонентов из всех доступных модулей-расширений для их корректного отображения в интерфейсе. Запуск devenv с данным ключом не открывает GUI окно IDE.

В PVS-Studio мы обходимся без запуска RegPkg, а вручную добавляем нужную информацию в реестр во время установки. Это делается из тех соображений, чтобы не зависеть от еще одной сторонней утилиты, а полностью самим контролировать процесс установки. Но мы всё же используем RegPkg при разработке плагина для удобства его отладки.

VSIX пакеты

Начиная с версии Visual Studio 2010, появилась возможность существенно упростить развёртывание VSPackage модулей с помощью VSIX пакетов. VSIX пакет представляет из себя стандартный OPC (Open Packaging Conventions) архив, содержащий бинарные файлы плагина и все необходимые для их развёртывания вспомогательные файлы. Данный архив может быть передан стандартной утилите VSIXInstaller.exe, которая автоматически зарегистрирует содержащиеся в нём модули-расширения.

VSIXInstaller.exe MyPackage.vsix

С помощью инсталлятора VSIX можно также удалить установленный пакет командой /uninstall, с указанием уникального GUID идентификатора модуля.

VSIXInstaller.exe /uninstall: 009084B1-6271-4621-A893-6D72F2B67A4D

Cодержимое VSIX контейнера задаётся через специальный файл vsixmanifest, который должен быть добавлен в проект плагина. Vsixmanifest позволяет задать:

  • поддерживаемые модулем целевую версию и редакцию среды Visual Studio для установки;
  • GUID модуля;
  • необходимые для регистрации компоненты (VSPackage, MEF component, toolbox control и т.д.);
  • информацию об устанавливаемом модуле (описание, лицензия, версия и т.п.).

Для включения в контейнер дополнительных файлов из проекта необходимо задать для этих файлов в MSBuild проекте узел IncludeInVSIX (можно также отметить такие файлы в SolutionExplorer через окно Properties).

<Content Include="MyPackage.pdb">
  <IncludeInVSIX>true</IncludeInVSIX>
</Content>

Фактически VSIX файл — это полноценный инсталлятор для расширений Visual Studio последних версий (20010 и 2012), позволяющий установить пакет методом "одного клика". Публикация VSIX пакета на официальном сайте IDE расширений Visual Studio Gallery позволит пользователю устанавливать такой модуль через диалог среды Tools -> Extension Manager.

VSIX позволяет развёртывать пакет расширения как в обычной редакции Visual Studio, так и в изолированных\интегрированных оболочках. В случае интеграции в изолированную оболочку в файле манифеста необходимо будет вместо версии Visual Studio указать специальную строку-идентификатор для такой целевой среды. Например, для среды Atmel Studio 6.1 такая строка будет выглядеть так: "AtmelStudio, 6.1". В случае если ваше расширение основано на взаимодействии с общими интерфейсами модели автоматизации (интерфейсы взаимодействия с текстовым редактором, с абстрактным деревом проектов и т.п.), и не использует никаких специфичных интерфейсов (например, таких, как интерфейсы взаимодействия с проектами Visual C++), никто не мешает вам указать в поддерживаемых версиях сразу как несколько редакций Visual Studio, так и несколько различных версий Isolated Shell. Это, в свою очередь, позволит получить один общий инсталлятор для широкого круга Visual Studio продуктов.

Появившиеся в VS2010 инсталляции в виде VSIX существенно облегчили пользователю (да и разработчику) установку расширений. Причем на столько, что некоторые разработчики плагинов делают только инсталлятор для VS2010, лишь бы не связываться с разработкой плагина и инсталлятора для старых версий Visual Studio.

На практике, к сожалению, как это часто бывает в программистском мире, возможны проблемы при использовании VSIX инсталлятора совместно с интерфейсом extension manager в Visual Studio 2010. В частности, в некоторых случаях бинарные файлы не всегда удаляются корректно, что блокирует работу как VSIX инсталлятора, так и студийного extension manager и вынуждает находить и удалять эти файлы вручную. Поэтому следует использовать VSIX с осторожностью, по возможности обеспечивая перед началом установки прямое удаление файлов от старой версии устанавливаемого плагина.

Package Load Key

Каждый загружаемый в Visual Studio модуль VSPackage должен содержать уникальный PLK (Package Load Key) ключ. PLK ключ задаётся через атрибут ProvideLoadKey класса Package для версий IDE 2005 и 2008.

[ProvideLoadKey("Standard", "9.99", "MyPackage", "My Company", 100)]

Начиная с Visual Studio 2010 наличие PLK и, соответственно, атрибута ProvideLoadKey, не является обязательным, однако его можно использовать в случае, если разрабатываемый модуль нацелен на несколько версий среды MSVS. Для получения PLK ключа необходимо зарегистрироваться на портале Visual Studio Industry Partner т.е. PLK ключ гарантирует, что в среде разработки загружены только пакеты, сертифицированные Microsoft. Однако для машин с установленным пакетом Visual Studio SDK делается исключение. Вместе с SDK устанавливается Developer License Key, позволяющий в дальнейшем загружать в соответствующей данной SDK среде Visual Studio любой модуль расширения, независимо от достоверности его PLK ключа.

И еще раз стоит напомнить о необходимости тестировать готовый дистрибутив на машине без установленного Visual Studio SDK. Дело в том, что если у вас задан неправильный ключ PLK, то на машине разработчика понять это будет непросто, так как модуль расширения будет работать.

Особенности регистрации расширений при поддержке нескольких версий Visual Studio

По умолчанию шаблон VSPackage генерирует проект расширения для текущей, используемой при разработке, версии Visual Studio. Однако это не является необходимым требованием, т.е. возможна разработка расширения для одной версии среды с использованием другой версии. Стоит помнить, что при автоматическом обновлении файла проекта до более новой версии через devenv /Upgrade целевая версия IDE и, соответственно, подключенные managed API библиотеки-обёртки останутся неизменными, т.е. от предыдущей версии Visual Studio.

Для изменения целевой версии Visual Studio (а точнее для регистрации плагина именно в этой версии среды) необходимо поменять значения, передаваемые атрибуту DefaultRegistryRoot (для версий 2005 и 2008, начиная с версии Visual Studio 2010 данный атрибут не нужен), или поменять целевую версию в файле манифеста VSIX (для версий после 2008).

Поддержка VSIX появилась только в Visual Studio 2010, поэтому для сборки и отладки плагина для более ранних версий из Visual Studio 2010 (и более новых версий) необходимо будет обеспечить выполнение всех ранее описанных шагов регистрации вручную, без использования манифеста VSIX. При изменении целевой версии IDE стоит не забывать менять и версии используемых в плагине managed библиотек-обёрток для COM интерфейсов.

Изменение целевой версии IDE для плагина затрагивает следующие атрибуты класса Package:

  • В атрибуте InstalledProductRegistration начиная с Visual Studio 2010 не поддерживается перегрузка конструктора с сигнатурой (Boolean, String, String, String);
  • Атрибуты DefaultRegistryRoot и ProvideLoadKey не обязательны для модулей, разрабатываемых только под версии Visual Studio не меньше 10-ой (т.е. начиная от Visual Studio 2010), т.к. схожие значения теперь задаются в файле манифеста VSIX;

Рекомендуемые ссылки

Объектная модель автоматизации Visual Studio. Интерфейсы EnvDTE и Visual Studio Shell Interop

В данном разделе проводится обзор объектной модели автоматизации Visual Studio. Рассматривается общая структура модели, описывается получение доступа к её интерфейсам с помощью объектов верхнего уровня DTE/DTE2, приводятся примеры использования некоторых её элементов. Также рассматривается вопрос использования интерфейсов модели в многопоточных приложениях и приводится реализация механизмов многопоточного взаимодействия с COM интерфейсами в managed коде.

Введение

Среда разработки Visual Studio построена на принципах автоматизации и расширяемости, позволяя разработчикам интегрировать в себя практически любые новые элементы и взаимодействовать как со стандартными, так и с пользовательскими компонентами. Для реализации данных задач пользователям Visual Studio предоставлено несколько взаимно-дополняющих друг друга инструментов. Самым базовым и универсальным из которых является объектная модель автоматизации Visual Studio.

Объектная модель автоматизации представляет собой ряд библиотек, содержащих обширный хорошо структурированный набор API, покрывающих все аспекты автоматизации IDE и большинство аспектов её расширяемости. Несмотря на то, что данная модель по сравнению с другими инструментами расширения IDE не предоставляет возможностей для взаимодействия с некоторыми областями Visual Studio (а в основном это касается расширения некоторого функционала IDE), она является наиболее гибким и универсальным таким средством.

Большинство интерфейсов модели автоматизации доступны для любого вида IDE расширений, в том числе позволяя взаимодействовать с ней и из внешнего независимого процесса. Более того, сама модель может быть расширена за счёт пользовательского расширения самой Visual Studio, делая тем самым доступными новые пользовательские компоненты для других разработчиков.

Структура объектной модели автоматизации

Объектная модель Visual Studio состоит из взаимосвязанных функциональных групп объектов, охватывающих все основные аспекты среды разработки, и предоставляет возможности для их управления и расширения. Доступ к любой из этих групп возможен через глобальный интерфейс верхнего уровня DTE (Development Tools Environment). На рисунке 1 приведена общая структура объектной модели автоматизации с разделением на функциональные группы.

Рисунок 1 — Visual Studio Automation Object Model (нажмите на рисунок для увеличения)

Рисунок 1 — Visual Studio Automation Object Model (нажмите на рисунок для увеличения)

Модель может быть расширена пользователем в следующих функциональных группах:

  • проектные модели (реализация новых типов проектов, поддержка новых языков);
  • документные модели (реализация новых типов документов и редакторов документов);
  • модели уровня редактора кода (реализация поддержка специфичных конструкций для новых языков);
  • модели сборочного процесса проектов.

Расширение модели автоматизации доступно только для модулей VSPackage.

Все интерфейсы модели автоматизации можно условно разделить на 2 большие группы. 1 группа – интерфейсы пространств имён EnvDTE и Visual Studio Interop, которые затрагивают взаимодействие с общими базовыми компонентами непосредственно самой среды Visual Studio, такими, как редакторы, инструментальные окна, службы обработки событий и т.п. 2-ая группа – это интерфейсы конкретной проектной модели. На рисунке выше эта группа интерфейсов обозначена через свойства позднего связывания (late-bound properties), т.е. эти интерфейсы реализованы в отдельной динамически-подгружаемой библиотеке. Каждая стандартная (т.е. включённая в дистрибутив Visual Studio по умолчанию) проектная модель, как например Visual C++ или Visual Basic, имеет свою реализацию данных интерфейсов. Сторонние разработчики также могут расширять модель автоматизации, добавляя поддержку собственных проектных моделей и предоставляя свою реализацию интерфейсов автоматизации.

Заметим, что интерфейсы из выделенной нами 1-ой группы достаточно универсальны и в большинстве случаев могут быть использованы при работе с любой проектной моделью или редакцией Visual Studio, в том числе и в изолированных\интегрированных оболочках (Visual Studio Isolated\Integrated Shell). В данном же разделе мы более подробно остановимся как раз на этой группе.

Тем не менее, несмотря на универсальность модели автоматизации, не все из представленных здесь групп интерфейсов могут быть одинаково использованы в расширениях различного типа. В частности, некоторые возможности модели будут недоступны для внешних процессов, т.к. они завязаны на такие специфичные типы модулей-расширений, как Add-In или VSPackage. Поэтому выбирая тип будущего разрабатываемого модуля, следует ориентироваться прежде всего на требуемый от него функционал.

Пространство имён Microsoft.VisualStudio.Shell.Interop также предоставляет ряд COM интерфейсов, позволяющих расширять и автоматизировать работу со средой Visual Studio из managed кода. Классы MPF (Managed Package Framework), которые мы использовали ранее, в частности, для создания VSPackage плагина, в своей основе также используют эти интерфейсы. Хотя такие интерфейсы автоматизации и не являются частью рассмотренной выше модели EnvDTE, для VS Package плагинов они дополняют эту модель дополнительной функциональностью, недоступной плагинам других типов.

Получение ссылок на объекты DTE/DTE2

Для создания приложения автоматизации Visual Studio прежде всего необходимо получить доступ непосредственно к самим объектам автоматизации. Для этого необходимо, во-первых, подключить правильные версии библиотек, содержащих необходимые managed обёртки для API среды в пространстве имён EnvDTE. Во-вторых, нужно получить ссылку на главный объект верхнего уровня модели автоматизации — интерфейс DTE2.

В процессе развития среды Visual Studio некоторые из объектов автоматизации претерпевали изменения и получали дополнительную функциональность. Для сохранения обратной совместимости с уже существовавшими модулями-расширениями, вместо обновления старых интерфейсов EnvDTE были созданы новые пространства имён EnvDTE80, EnvDTE90, EnvDTE100 и т.п. Большинство подобных новых интерфейсов имеют такие же имена, что и в EnvDTE, но с добавлением на конце порядкового номера, например, Solution и Solution2. Рекомендуется использовать новые версии интерфейсов при создании нового проекта, поскольку именно они содержат наиболее полную функциональность. Стоит заметить, что поля и методы интерфейса DTE2 возвращают объекты, типы которых соответствуют интерфейсу DTE, т.е. при обращении к dte2.Solution возвращается Solution, а не Solution2, как может показаться.

Несмотря на то, что новые пространства имён EnvDTE80, EnvDTE90, EnvDTE100 содержат некоторую новую функциональность, именно в EnvDTE всё еще находится основная часть объектов автоматизации. Поэтому, чтобы иметь доступ ко всем существующем интерфейсам, необходимо подключить к проекту все версии managed библиотек-обёрток COM интерфейсов, а также получить ссылки как на DTE, так и на DTE2.

Способ получения ссылки на верхний объект EnvDTE зависит от типа разрабатываемого расширения Visual Studio. Далее рассмотрим 3 вида расширений: Add-In, VSPackage и независимый от MSVS внешний процесс.

Add-In расширение

В случае разработки модуля вида Add-In, доступ к DTE интерфейсу можно получить в методе OnConnection, который должен быть реализован для интерфейса IDTExtensibility, предоставляющего доступ к событиям взаимодействия среды и Add-In модулей. Метод OnConncetion вызывается в момент загрузки модуля в IDE, что может произойти как непосредственно в момент загрузки среды, так и после первого обращения к нему. Получение ссылки выглядит следующим образом:

public void OnConnection(object application,
  ext_ConnectMode connectMode, object addInInst, ref Array custom)
           {
               _dte2 = (DTE2)application;
               ...
           }

Add-In модуля может быть инициализирован как в момент загрузки самой среды, так и после первого к нему обращения в текущем сеансе работы IDE. Поэтому, для определения момента загрузки в методе OnConnection можно использовать параметр connectMode.

switch(connectMode)
{
    case ext_ConnectMode.ext_cm_UISetup:
        ...
        break;

    case ext_ConnectMode.ext_cm_Startup:
        ...
        break;

    case ext_ConnectMode.ext_cm_AfterStartup:
        ...
        break;

    case ext_ConnectMode.ext_cm_CommandLine:
        ...
        break;
}

Как видно из примера, Add-in может быть загружен одновременно с загрузкой самой IDE (установлен как startup в Add-In manager), при первом обращении к нему, либо при вызове его из командной строки. Вариант ext_ConnectMode.ext_cm_UISetup вызывается только 1 раз за время существования плагина, при его самой первой загрузке. Этот вариант предназначен для инициализации интегрируемых в среду UI элементов (что будет рассмотрено подробнее позже).

Если Add-In загружается при старте Visual Studio (ext_ConnectMode.ext_cm_Startup), то в момент получения управления методом OnConnect среда может ещё быть не полностью проинициализирована. В таком случае инициализацию непосредственно DTE интерфейса предпочтительно отложить до окончания инициализации самой среды. Для этого можно воспользоваться предоставляемым интерфейсом IDTExtensibility обработчиком OnStartupComplete.

public void OnStartupComplete(ref Array custom)
{
    ...
}

VSPackage расширение

Для расширения типа VSPackage интерфейс DTE может быть получен как глобальный сервис Visual Studio с помощью метода GetService класса Package:

DTE dte = MyPackage.GetService(typeof(DTE)) as DTE;

Обратите внимание, что метод GetService может возвращать и null в случае, если Visual Studio не была полностью загружена и инициализирована, то есть находится в так называемом "zombie" состоянии. Для корректной обработки данной ситуации можно отложить получение ссылки на интерфейс DTE до момента, пока она не потребуется. Если же доступ к ссылке на DTE нужен непосредственно внутри метода Initialize, то можно воспользоваться интерфейсом IVsShellPropertyEvents (унаследовав от него наш подкласс Package), и тогда получить ссылку в обработчике OnShellPropertyChange.

DTE dte;
uint cookie;
 
protected override void Initialize()
{
  base.Initialize();

  IVsShell shellService = GetService(typeof(SVsShell)) as IVsShell;
  if (shellService != null)
    ErrorHandler.ThrowOnFailure(
      shellService.AdviseShellPropertyChanges(this,out cookie));
...
}
 
public int OnShellPropertyChange(int propid, object var)
{
  // when zombie state changes to false, finish package initialization
  if ((int)__VSSPROPID.VSSPROPID_Zombie == propid)
  {
    if ((bool)var == false)
    {
     this.dte = GetService(typeof(SDTE)) as DTE;
     IVsShell shellService = GetService(typeof(SVsShell)) as IVsShell;

     if (shellService != null)
       ErrorHandler.ThrowOnFailure(
         shellService.UnadviseShellPropertyChanges(this.cookie) );
     this.cookie = 0;
    }
  }
  return VSConstants.S_OK;
}

Следует обратить внимание на то, что в разных версиях Visual Studio процесс инициализации VSPackage модуля на старте IDE может происходить по-разному. Так в VS2005 и VS2008, благодаря их относительно быстрой загрузке, попытка доступа к DTE будет гарантированно возвращать null. В Visual Studio 2010 же может сложиться впечатление, что в Initialize() уже можно получить к DTE доступ. Однако данное впечатление обманчиво, поскольку такое использование DTE приводит к появлению "плавающих" трудно отлавливаемых ошибок, а сам DTE оказывается не полностью инициализированнымв момент получения ссылки. Поэтому не стоит пренебрегать описанным выше универсальным методом обработки состояния загрузки IDE для всех версий Visual Studio.

Независимый внешний процесс

DTE является верхне-уровневой абстракцией для самой среды Visual Studio в модели автоматизации. Для получения ссылки на него из внешнего приложения нужно использовать его ProgID COM идентификатор, например VisualStudio.DTE.10.0 для Visual Studio 2010. Приведём пример инициализации нового экземпляра IDE и получения ссылки на её DTE интерфейс.

// Get the ProgID for DTE 8.0.
System.Type t = System.Type.GetTypeFromProgID(
    "VisualStudio.DTE.10.0", true);
// Create a new instance of the IDE.
object obj = System.Activator.CreateInstance(t, true);
// Cast the instance to DTE2 and assign to variable dte.
EnvDTE80.DTE2 dte = (EnvDTE80.DTE2)obj;
// Show IDE Main Window
dte.MainWindow.Activate();

В поведённом примере мы фактически создаём новый объект DTE, запуская процесс devenv.exe методом CreateInstance. При этом GUI окно среды будет отображено только после вызова метода Activate. Простейший же способ получить ссылку на DTE из уже запущенного экземпляра Visual Studio будет выглядеть так:

EnvDTE80.DTE2 dte2;
dte2 = (EnvDTE80.DTE2)
  System.Runtime.InteropServices.Marshal.GetActiveObject(
    "VisualStudio.DTE.10.0");

Однако если было запущено несколько экземпляров IDE, то метод GetActiveObject вернёт ссылку только на интерфейс самого первого из запущенных экземпляра среды. Рассмотрим один из возможных вариантов получения DTE интерфейса у запущенного экземпляра Visual Studio по PID его процесса.

using EnvDTE80;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

[DllImport("ole32.dll")]
private static extern void CreateBindCtx(int reserved, 
                                         out IBindCtx ppbc);
[DllImport("ole32.dll")]
private static extern void GetRunningObjectTable(int reserved, 
  out IRunningObjectTable prot);

public static DTE2 GetByID(int ID)
{
  //rot entry for visual studio running under current process.
  string rotEntry = String.Format("!VisualStudio.DTE.10.0:{0}", ID);
  IRunningObjectTable rot;
  GetRunningObjectTable(0, out rot);
  IEnumMoniker enumMoniker;
  rot.EnumRunning(out enumMoniker);
  enumMoniker.Reset();
  IntPtr fetched = IntPtr.Zero;
  IMoniker[] moniker = new IMoniker[1];
  while (enumMoniker.Next(1, moniker, fetched) == 0)
  {
    IBindCtx bindCtx;
    CreateBindCtx(0, out bindCtx);
    string displayName;
    moniker[0].GetDisplayName(bindCtx, null, out displayName);
    if (displayName == rotEntry)
    {
      object comObject;
      rot.GetObject(moniker[0], out comObject);
      return (EnvDTE80.DTE2)comObject;
    }
  }
  return null;
}

Здесь мы получили интерфейс DTE, идентифицировав нужный нам экземпляр IDE в таблице запущенных COM объектов (ROT, Running Object Table) по его идентификатору. Теперь мы можем получить DTE для любого из запущенных экземпляров Visual Studio, например:

Process Devenv;
...
//Get DTE by Process ID
EnvDTE80.DTE2 dte2 = GetByID(Devenv.Id);

Дополнительно, для получения проектно-специфичных интерфейсов (в том числе и пользовательских расширений модели), например для проектной модели CSharpProjects, через имеющийся DTE объект, можно воспользоваться методом GetObject:

Projects projects = (Projects)dte.GetObject("CSharpProjects");

Метод GetObject вернёт нам множество Projects стандартных Project объектов, каждый из которых будет, помимо общих в модели автоматизации свойств, содержать ссылку и на специфичный для данной модели тип.

Документы текстового редактора Visual Studio

Модель автоматизации определяет текстовые документы Visual Studio через интерфейс текстового документа TextDocument. Исходные C/C++ файлы открываются средой как текстовые документы. TextDocument основан на общем интерфейсе документов модели автоматизации (интерфейс Document), описывающем любой открытый в редакторе или дизайнере Visual Studio файл. Ссылку на объект текстового документа можно получить через поле Object объекта Document. Получим, например, текстовый документ для активного (т.е. открытого и имеющего фокус) документа из текстового редактора IDE.

EnvDTE.TextDocument objTextDoc =
(TextDocument)PVSStudio.DTE.ActiveDocument.Object("TextDocument");

Редактирование документов

Интерфейс TextSelection позволяет модифицировать текст и управлять выделениями. Методы данного интерфейса отражают функционал текстового редактора Visual Studio, т.е. позволяют работать непосредственно с видимым в UI текстом.

EnvDTE.TextDocument Doc =
  (TextDocument)PVSStudio.DTE.ActiveDocument.Object(string.Empty);
Doc.Selection.SelectLine();
TextSelection Sel = Doc.Selection;
int CurLine = Sel.TopPoint.Line;
String Text = Sel.Text;
Sel.Insert("test\r\n");

В данном примере мы выделяем строку текста под курсором, считываем выделенные текст и заменяем его на строку 'test'.

Также интерфейс TextDocument позволяет редактировать текст документа с помощью интерфейса EditPoint. Данный интерфейс схож с интерфейсом TextSelection, но в отличие от него позволяет манипулировать данными текстового буфера, а не текстом, отображённым непосредственно в редакторе. Отличие в том, что на текстовый буфер не влияют такие параметры редактора, как WordWrap и Virtual Spaces. Заметим, что для данных методов редактирования не доступны участки текста из read-only блоков.

Приведём пример редактирования с помощью EditPoint, в котором текст добавляется в конце текущей строки редактирования (строки с курсором).

objEditPt = objTextDoc.StartPoint.CreateEditPoint();
int lineNumber = objTextDoc.Selection.CurrentLine;
objEditPt.LineDown(lineNumber - 1);

EditPoint objEditPt2 = objTextDoc.StartPoint.CreateEditPoint();
objEditPt2.LineDown(lineNumber - 1);
objEditPt2.CharRight(objEditPt2.LineLength);

String line = objEditPt.GetText(objEditPt.LineLength);
String newLine = line + "test";
objEditPt.ReplaceText(objEditPt2, newLine,
  (int)vsEPReplaceTextOptions.vsEPReplaceTextKeepMarkers);

Навигация по документам

Для модулей типа VSPackage доступен ряд глобальных служб среды, позволяющих открывать и просматривать документы. Данные службы можно получить с помощью Managed Package Framework метода Package.GetGlobalService(). Заметим, что используемые здесь интерфейсы не являются частью модели EnvDTE, доступны только для объекта пакета расширения Package и поэтому не могут быть использованы в других видах расширений Visual Studio. Тем не менее, эти службы удобны для использования при работе с IDE документами совместно с описанным выше интерфейсом Document, поэтому мы остановимся на них более подробно в данном разделе.

Интерфейс IVsUIShellOpenDocument позволяет контролировать состояние открытых документов среды. Приведём пример открытия документа через путь до связанного с ним файла.

String path = "C:\Test\test.cpp";
IVsUIShellOpenDocument openDoc =
  Package.GetGlobalService(typeof(IVsUIShellOpenDocument))
    as IVsUIShellOpenDocument;

IVsWindowFrame frame;
Microsoft.VisualStudio.OLE.Interop.IServiceProvider sp;
IVsUIHierarchy hier;
uint itemid;
Guid logicalView = VSConstants.LOGVIEWID_Code;
if (ErrorHandler.Failed(
  openDoc.OpenDocumentViaProject(path, ref logicalView, out sp, 
    out hier, out itemid, out frame))
      || frame == null)
{
  return;
}
object docData;
frame.GetProperty((int)__VSFPROPID.VSFPROPID_DocData, out docData);

Файл либо будет открыт в новом редакторе, либо его редактор получит фокус, если он уже был открыт ранее. Далее получим из открытого документа текстовый буфер типа VsTextBuffer.

// Get the VsTextBuffer  
VsTextBuffer buffer = docData as VsTextBuffer;
if (buffer == null)
{
  IVsTextBufferProvider bufferProvider = docData as
    IVsTextBufferProvider;
  if (bufferProvider != null)
  {
    IVsTextLines lines;
    ErrorHandler.ThrowOnFailure(bufferProvider.GetTextBuffer(
      out lines));
    buffer = lines as VsTextBuffer;
    Debug.Assert(buffer != null, 
      "IVsTextLines does not implement IVsTextBuffer");
    if (buffer == null)
    {
      return;
    }
  }
} 

Интерфейс IVsTextManager позволяет управлять всеми текстовыми буферами, активными в среде. Например, можно произвести навигацию в текстовом документе с помощью метода текстового менеджера NavigateToLineAndColumn в полученном выше буфере:

IVsTextManager mgr =
  Package.GetGlobalService(typeof(VsTextManagerClass))
  as IVsTextManager;
mgr.NavigateToLineAndColumn(buffer, ref logicalView, line, 
  column, line, column);

Подписка и обработка событий

События объектов автоматизации определены в корневом элементе DTE.Events. При этом данный интерфейс содержит ссылки как на стандартные (общие) события IDE (CommandEvents, SolutionEvents), так и на события различных элементов среды (отдельные типы проектов, редакторы, инструменты и т.п.), в том числе и определённых пользователем. Для получения ссылки для подобного объекта автоматизации можно использовать метод GetObject.

При подписке на события DTE стоит помнить, что в момент инициализации расширения данный интерфейс может быть ещё не доступен. Поэтому важно помнить и учитывать порядок инициализации вашего разрабатываемого модуля в случае, если подписка на DTE.Events потребуется именно в момент инициализации плагина-расширения. При этом правильная обработка этого момента для получения ссылки на DTE будет выглядеть по-разному для разных версий расширений, как было описано в предыдущем разделе.

Получим ссылку для событий проектной модели Visual C++, определённой интерфейсом VCProjectEngineEvents и назначим обработчик на удаление элемента из Solution дерева:

VCProjectEngineEvents m_ProjectItemsEvents =
  PVSStudio.DTE.Events.GetObject("VCProjectEngineEventsObject") 
    as VCProjectEngineEvents;
m_ProjectItemsEvents.ItemRemoved +=
  new _dispVCProjectEngineEvents_ItemRemovedEventHandler(
    m_ProjectItemsEvents_ItemRemoved);

События MDI окон

Для обработки стандартных событий MDI окна среды можно воспользоваться интерфейсом Events.WindowEvents. Данный интерфейс позволяет назначить как отдельный обработчик для окна (определённого через интерфейс EnvDTE.Window), так и общий обработчик для всех окон среды.

Назначение общего обработчика для среды на переключение между окнами:

WindowEvents WE = PVSStudio.DTE.Events.WindowEvents;
WE.WindowActivated += 
  new _dispWindowEvents_WindowActivatedEventHandler(
    Package.WE_WindowActivated);

Назначение обработчика на переключение для активного в данный момент окна возможно через индексатор свойства WindowEvents:

WindowEvents WE =
  m_dte.Events.WindowEvents[MyPackage.DTE.ActiveWindow];
WE.WindowActivated += new
  _dispWindowEvents_WindowActivatedEventHandler(
    MyPackage.WE_WindowActivated);

События IDE команд

Непосредственно работа с командами и их расширение через модель автоматизации рассматриваются в отдельном разделе. Здесь мы затронем вопрос обработки событий команд (но не самого выполнения команд). Назначение обработчиков на события возможно с помощью интерфейса Events.CommandEvents. Свойство CommandEvents, по аналогии с обработкой событий MDI окон, также позволяет назначить обработчик как для всех команд IDE, так и для конкретной команды с помощью индексатора.

Назначение обработчика команд среды на событие завершения выполнения команды:

CommandEvents CEvents = DTE.Events.CommandEvents;
CEvents.AfterExecute += new
  _dispCommandEvents_AfterExecuteEventHandler(C_AfterExecute);

Для назначения обработчика конкретной команды необходимо сначала идентифицировать эту команду. Каждая команда среды идентифицируется парой GUID:ID, причём для пользовательских команд эти значения задаются непосредственно при интеграции команды разработчиком, например через таблицы команд (VSCT). Visual Studio имеет специальный отладочный режим работы, позволяющий узнать идентификаторы любой команды. Для активации этого режима нужно добавить следующий ключ в системный реестр (пример для Visual Studio 2010):

[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\10.0\General]
"EnableVSIPLogging"=dword:00000001

После перезапуска среды с этим ключом, наведение курсора мыши (иногда не срабатывает сразу, если не кликнуть) с зажатыми клавишами CTRL+SHIFT вызывает диалоговое окно, в котором приведены все внутренние идентификаторы команды. Из них нас интересуют значения Guid и CmdID. Пример назначения обработчика для команды File.NewFile:

CommandEvents CEvents = DTE.Events.CommandEvents[
  "{5EFC7975-14BC-11CF-9B2B-00AA00573819}", 221];
CEvents.AfterExecute += new  
  _dispCommandEvents_AfterExecuteEventHandler(C_AfterExecute);

Обработчик, назначенный подобным образом, получает управление после выполнения команды.

void C_AfterExecute(string Guid, int ID, object CustomIn, 
  object CustomOut)
{
  ...
}

Не стоит путать этот обработчик (после завершению выполнения) с непосредственным обработчиком выполнения самой команды, который может быть назначен при её инициализации (из модуля-расширения в случае, если команда является пользовательской). Подробно работа с командами среды будет описана в специальном посвященном IDE командам разделе.

В заключение данного подраздела заметим, что при разработке пакета-расширения (VSPackage) PVS-Studio мы столкнулись с необходимостью хранить ссылки на объекты интерфейса, содержащие в свою очередь наши делегаты-обработчики (такие, как CommandEvents, WindowEvents и прочие), в качестве полей нашего главного подкласса Package. При назначении же обработчика через локальную переменную, определённую внутри самой функции, данный обработчик терялся сразу после выхода из неё. Похоже, что это происходит в результате действий сборщика мусора .NET. Причём происходит это несмотря на то, что мы получаем ссылку на такой объект из интерфейса DTE, однозначно существующего в течение всего времени жизни нашего модуля-расширения.

Обработка событий проектов и решений (для VSPackage модулей)

Рассмотрим несколько интерфейсов из пространства имён Microsoft.VisualStudio.Shell.Interop, позволяющих обрабатывать события непосредственно проектов и решений в среде Visual Studio. Хотя эти интерфейсы и не относятся непосредственно к модели автоматизации EnvDTE, они могут быть реализованы основным классом модуля расширения VS Package (т.е. классом, наследуемым от Package из Managed Package Framework). Поэтому, в случае разработки плагина такого типа эти интерфейсы удобно дополняют базовый набор, предоставленный объектом DTE. Это, кстати, является ещё одним доводом в пользу разработки полноценного плагина именно с помощью MPF.

Интерфейс IVsSolutionEvents может быть реализован классом, наследуемым от Package, в версиях Visual Studio, начиная от Visual Studio 2005, и основанных на них изолированных\интегрированных оболочках среды (Visual Studio isolated\integrated shell). Этот интерфейс позволяет отслеживать загрузку, выгрузку, открытие и закрытие проектов и целых решений в среде разработки путём реализации таких методов интерфейса, как OnAfterCloseSolution, OnBeforeCloseProject, OnQueryCloseSolution и т.д. Например:

        public int OnAfterLoadProject(IVsHierarchy pStubHierarchy, 
                                       IVsHierarchy pRealHierarchy)
        {
//your custom handler code            
return VSConstants.S_OK;
        }

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

Интерфейс IVsSolutionLoadEvents, аналогично рассмотренному выше интерфейсу, должен быть реализован подклассом Package и доступен для версий среды начиная с Visual Studio 2010. Этот интерфейс позволит отслеживать такие интересные моменты, как погрузка группы проектов в пакетном режиме и отложенная (фоновая) подгруздка решения (методы OnBeforeLoadProjectBatch и OnBeforeBackgroundSolutionLoadBegins), а также отловить завершение фоновой погрузки (метод OnAfterBackgroundSolutionLoadComplete). Эти обработчики могут оказаться незаменимыми в случае, если вашему плагину необходимо выполнить какой-нибудь код сразу после своей инициализации, и при этом он имеет зависимости как раз от загруженных проектов\решений. Выполнение подобного кода без ожидания полной загрузки решения может в таком случае привести как к некорректным (неполным) результатом в связи с тем, что дерево проектов построено не полностью, так и к runtime исключениям.

При разработке Visual Studio плагина PVS-Studio мы столкнулись с ещё одним интересным аспектом инициализации VSPackage модулей. Так если один Package модуль, не завершив своей инициализации, находится в состоянии ожидания (например, показывая пользователю диалог), то инициализация других модулей-расширений блокируется. Поэтому, обрабатывая события загрузки и инициализации в среде, следует помнить и о таком сценарии.

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

class MyPackage: Package, IVsSolutionLoadEvents, IVsSolutionEvents
{
//Implementation of Package, IVsSolutionLoadEvents, IVsSolutionEvents
...
}

Поддержка цветовой схемы среды Visual Studio

В случае если разрабатываемый вами плагин интегрируется в интерфейс среды разработки, например добавлением в неё собственных инструментальных или документных MDI окон (а такую интеграцию удобнее всего реализовать на основе VSPackage модуля), цветовую схему ваших интерфейсных компонентов желательно привести в соответствие с общей цветовой схемой среды Visual Studio. Особенно актуальной эта задача стала с появление в Visual Studio 2012 двух сильно отличающихся цветовых схем (Dark и Light), которые пользователь может переключать в настройках среды "на лету".

Для "подхватывания" цветовых настроек среды можно воспользоваться методом GetVSSysColorEx интерфейса IVsUIShell2 из Visual Studio Interop. Данный интерфейс будет доступен только VSPackage модулям.

IVsUIShell2 vsshell = this.GetService(typeof(SVsUIShell)) as 
  IVsUIShell2;

С помощью перечислений __VSSYSCOLOREX и __VSSYSCOLOREX3, передаваемых в метод GetVSSysColorEx , вы можете получить текущий установленный в Visual Studio цвет каждого из её интерфейсных компонент. Например, получим один из цветов градиента у фона контекстного меню:

uint Win32Color;
vsshell.GetVSSysColorEx((int)
  __VSSYSCOLOREX3.VSCOLOR_COMMANDBAR_MENU_BACKGROUND_GRADIENTBEGIN,
  out Win32Color);
Color BackgroundGradient1 = 
  ColorTranslator.FromWin32((int)Win32Color);

Теперь можно использовать полученный объект Color для "раскраски" собственных контекстных меню. Для того чтобы определить момент, когда необходимо обновить цветовую схему ваших UI компонентов, вы можете воспользоваться, например, событиями команды, открывающей экран настроек среды (Tools -> Options), подписавшись на соответствующий обработчик, как это было описано выше данном разделе.

Если же вы не имеете по какой-то причине доступа к объекту IVsUIShell2 (например, при разработке плагина не VSPackage типа), но вам все же необходимо поддержать цветовую схему Visual Studio, то вы можете взять значения цветов отдельных UI компонентов в обход интерфейсов оптимизации, напрямую из системного реестра. В данной статье мы не будем останавливаться на этом методе подробно, но вы можете скачать отсюда бесплатную и открытую утилиту для редактирования цветовых тем Visual Studio. Эта утилита написана на C# и содержит весь необходимый код для чтения и модификации цветовых тем Visual Studio 2012 из managed приложений.

Взаимодействие с COM интерфейсами в многопоточном приложении

Изначально пакет расширения PVS-Studio не содержал в себе каких-то особенных механизмов для обеспечения потоковой безопасности при работе с Visual Studio API. При этом мы старались ограничить всё взаимодействие с данными интерфейсами в рамках одного, создаваемого нашим плагином, фонового потока. Данный подход работал без видимых проблем долгое время. Однако сообщения о схожих ComException ошибках от нескольких наших пользователей побудили нас более детально разобраться в данном вопросе и реализовать собственный механизм для обеспечения потоковой безопасности в COM Interop.

Хотя объектная модель автоматизации Visual Studio не является потоково-безопасной, она предоставляет возможности для взаимодействия с многопоточными приложениями. Приложение Visual Studio является COM (Component Object Mode) сервером. Для обращения COM-клиентов (в данном случае это наш модуль-расширение) к потоково-небезопасным серверам технология COM предоставляет механизм, известный как STA (single-threaded apartment) модель. В терминах COM Apartment представляет собой логический контейнер внутри процесса, в котором объекты и потоки имеют общие права межпотокового доступа. STA позволяет содержать в таком контейнере только один поток, но неограниченное количество объектов. Обращения же из других потоков к таким потоково-небезопасным объектам в STA преобразуются в сообщения и помещаются в очередь сообщений. Затем сообщения поочерёдно достаются из этой очереди и преобразуются в вызовы методов потоком, находящимся в STA, что делает возможным обращение к этим небезопасным объектам на сервере только из одного потока.

Использование Apartment механизма в managed коде

Непосредственно .NET Framework не использует Apartment механизм COM. Поэтому, когда в ситуации COM взаимодействия managed приложение обращается к COM объекту, CLR (Common Language Runtime) должен создать и инициализировать apartment контейнер. Managed поток может создать и войти как в MTA (multi-threaded apartment, контейнер, который, в противовес STA, может содержать несколько потоков одновременно), так и в STA, причём, по умолчанию, поток будет запущен именно в MTA. Задать apartment можно с помощью метода Thread.SetApartmentState перед непосредственным запуском потока:

Thread t = new Thread(ThreadProc);
t.SetApartmentState(ApartmentState.STA);
...
t.Start();

Т.к. apartment не может быть изменён для уже запущенного потока, для его установки в основном потоке в managed приложения в STA необходимо воспользоваться атрибутом STAThread:

[STAThread]
static void Main(string[] args)
{...}

Реализация фильтра ошибок доступа к COM интерфейсам в managed среде

Так как в STA все обращения к COM серверу сериализуются, один из вызывающих клиентов может быть блокирован или отклонён в моменты, когда сервер занят, обрабатывает другие обращения или другой поток уже находится в apartment контейнере. В случае, когда COM сервер отклоняет обращение клиента, .NET COM Interop генерирует исключения вида System.Runtime.InteropServices.COMException ("The message filter indicated that the application is busy").

В случае разработки встраиваемого в Visual Studio модуля (VSPackage, Add-In) или макроса, управление в него передаётся обычно из главного STA UI потока среды (перехват event'ов, обработка изменений состояний и т.п.). Обращение к COM интерфейсам автоматизации из этого основного потока является безопасным. Однако если планируется создание других фоновых потоков и обращение к интерфейсам EnvDTE из них (например, длительные вычисления, которые могут привести к зависанию интерфейса среды), то желательно реализовать механизм для обработки отклонённых сервером вызовов.

Наиболее часто с подобными COM Exception ошибками мы сталкивались при работе с PVS-Studio в окружении других установленных в Visual Studio плагинов при одновременном взаимодействием самого пользователя с интерфейсом IDE. Вполне закономерно, что подобная ситуация приводила к одновременным параллельным запросам объектов, находящихся в STA, и соответственно, отклонению части из этих запросов.

Для выборочной обработки входящих и исходящих сообщений COM предоставляет интерфейс IMessageFilter. Если сервер реализует его, то все запросы поступают в метод HandleIncomingCall, а клиент информируется об отклонённом запросе через метод RetryRejectedCall. При этом появляется возможность либо повторить запрос, либо корректно отработать отказ на него (например, показав пользователю соответствующий диалог о занятости сервера). Далее приведём пример реализации обработки отклонённого запроса в managed приложении.

[ComImport()]
[Guid("00000016-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMessageFilter
{
  [PreserveSig]
  int HandleInComingCall(
    int dwCallType,
    IntPtr hTaskCaller,
    int dwTickCount,
    IntPtr lpInterfaceInfo);

  [PreserveSig]
  int RetryRejectedCall(
    IntPtr hTaskCallee,
    int dwTickCount,
    int dwRejectType);

  [PreserveSig]
  int MessagePending(
    IntPtr hTaskCallee,
    int dwTickCount,
    int dwPendingType);
}

class MessageFilter : MarshalByRefObject, IDisposable, IMessageFilter
{

  [DllImport("ole32.dll")]
  [PreserveSig]
  private static extern int CoRegisterMessageFilter(
    IMessageFilter lpMessageFilter, 
    out IMessageFilter lplpMessageFilter);

  private IMessageFilter oldFilter;
  private const int SERVERCALL_ISHANDLED = 0;
  private const int PENDINGMSG_WAITNOPROCESS = 2;
  private const int SERVERCALL_RETRYLATER = 2;

  public MessageFilter()
  {
    //Starting IMessageFilter for COM objects
    int hr =
      MessageFilter.CoRegisterMessageFilter(
        (IMessageFilter)this, 
         out this.oldFilter);
    System.Diagnostics.Debug.Assert(hr >= 0, 
      "Registering COM IMessageFilter failed!");
  }

  public void Dispose()
  {
    //disabling IMessageFilter
    IMessageFilter dummy;
    int hr = MessageFilter.CoRegisterMessageFilter(this.oldFilter, 
                                                   out dummy);
    System.Diagnostics.Debug.Assert(hr >= 0, 
      "De-Registering COM IMessageFilter failed!")
    System.GC.SuppressFinalize(this);
  }

  int IMessageFilter.HandleInComingCall(int dwCallType, 
    IntPtr threadIdCaller, int dwTickCount, IntPtr lpInterfaceInfo)
  {
    // Return the ole default (don't let the call through).
    return MessageFilter.SERVERCALL_ISHANDLED;
  }

  int IMessageFilter.RetryRejectedCall(IntPtr threadIDCallee, 
    int dwTickCount, int dwRejectType)
  {
    if (dwRejectType == MessageFilter.SERVERCALL_RETRYLATER)
    {
      // Retry the thread call immediately if return >=0 & 
      // <100.
      return 150; //waiting 150 mseconds until retry
    }
    // Too busy; cancel call. SERVERCALL_REJECTED
    return -1;
    //Call was rejected by callee. 
    //(Exception from HRESULT: 0x80010001 (RPC_E_CALL_REJECTED))
  }

  int IMessageFilter.MessagePending(
      IntPtr threadIDCallee, int dwTickCount, int dwPendingType)
  {
    // Perform default processing.
    return MessageFilter.PENDINGMSG_WAITNOPROCESS;
  }
}

Теперь мы сможем использовать MessageFilter при работе с COM интерфейсами из фонового потока:

using (new MessageFilter())
{
  //COM-interface dependent code
  ...
}

Рекомендуемые ссылки

Команды Visual Studio

В данном разделе рассматриваются методы программного создания, использования и обработки команд Visual Studio в модулях-расширениях данной среды с помощью интерфейсов объектной модели автоматизации и IDE служб. Показана связь между IDE командами и UI элементами среды через пользовательские меню и панели инструментов.

Введение

Команды Visual Studio позволяют напрямую взаимодействовать со средой разработки с помощью клавиатуры. Большинство функционала различных диалоговых и инструментальных окон среды и инструментальных панелей представлено командами. Пункты главного меню приложения и кнопки панелей инструментов фактически являются командами. Команды могут и не иметь непосредственного представления в UI среды разработки, они не являются непосредственно элементами интерфейса IDE, но могут быть ими представлены, как, например, в случае с пунктами главного меню.

Модуль-расширение PVS-Studio для IDE в качестве одного их своих основных UI компонентов (другим таким компонентом является инструментальное окно) интегрирует в главное меню Visual Studio несколько собственных подгрупп команд, позволяя пользователю контролировать все аспекты использования статического анализа как из самой среды, так и через прямой вызов команд из командной строки.

Использование команд

Любая IDE команда, независимо от формы её представления (или отсутствия такового) в интерфейсе IDE, может быть исполнена напрямую через окна Command Window и Immediate Window, а также с помощью аргумента командной строки devenv.exe /command при запуске приложения из консоли.

Полное имя команды формируется в соответствии с её принадлежностью к какой-либо функциональной группе, например команды пункта главного меню среды File. Её полное имя можно посмотреть в диалоге Keyboard, Environment страницы настроек Options. Диалог Tools -> Customize -> Commands позволяет посмотреть зарегистрированные в среде команды, отсортированные по группам и способам отображения в интерфейсе (меню, панели инструментов), а также редактировать их, удалять или добавлять новые команды.

Команды могут принимать дополнительные аргументы, передаваемые через пробел. Приведём пример вызова стандартной системной команды меню File -> New -> File, с передачей ей дополнительных параметров, через окно среды Command Window:

>File.NewFile Mytext /t:"General\Text File" 
  /e:"Source Code (text) Editor"

Синтаксис написания команд подчиняется следующим правилам:

  • Имя команды и её аргументы разделяются пробелом
  • Содержащие пробелы значения аргументов оборачиваются в кавычки
  • Знак вставки (^) используется для экранирования символов
  • Односимвольные сокращения имён аргументов можно сочетать, например /case (/c) и /word (/w) могут быть записаны как /cw

При использовании параметра запуска command, имя команды вместе со всеми её аргументами следует обернуть в двойные кавычки:

devenv.exe /command "MyGroup.MyCommandName arg1 arg2"

Для быстрого вызова команде может быть назначен псевдоним с помощью команды alias:

>alias MyAlias File.NewFile MyFile

Команды, добавляемые в главное меню IDE модулем-расширением PVS-Studio, могут также быть использованы и через вызов /command, в частности, например, для решения задачи интеграции статического анализа в автоматизированный сборочный процесс проекта. Здесь заметим, что непосредственно сам статический анализатор PVS-Studio.exe представляет собой как раз консольное приложение, работающее по схожему с компилятором принципу, т.е. он получает на вход путь до файла с исходным кодом и параметры компиляции этого файла, выдавая результаты своего анализа в потоки stdout/strerr. Понятно и то, что анализатор может быть относительно легко интегрирован непосредственно в сборочную систему (например, основанную на том же MSBuild, NMake или даже GNU Make) на одном уровне с вызовом C/C++ компилятора. Подобная система уже сама по определению будет реализовывать обход всех исходных файлов с предоставлением параметров компиляции для каждого из них, что фактически позволяет подменять (или скорее дополнять) вызов компилятора вызовом анализатора. И хотя такой режим работы полностью поддерживается анализатором PVS-Studio.exe, подобная интеграция анализа в сборочный процесс требует достаточно близкого знакомства непосредственно со сборочной системой, ровно как и собственно возможности для такой её модификации, что может быть как затруднительно, так и вовсе не доступно.

Поэтому для интеграции статического анализа PVS-Studio в автоматизированный сборочный процесс, можно на более высоком уровне (т.е. уже непосредственно на уровне сервера непрерывной интеграции) использовать вызов команд модуля-расширения в Visual Studio через /command, например, команды проверки PVS-Studio.CheckSolution. Конечно, такой вариант возможен только при использовании для сборки нативных проектных решений Visual C++ (vcproj/vcxproj).

В случае запуска Visual Studio из командной строки с аргументом /command, команда будет выполнена сразу после загрузки среды. При этом среда разработки будет запущена как обычное UI приложение, соответственно, и без перенаправления в запускающую её консоль стандартных потоков ввода/вывода. Стоит заметить, что в общем случае Visual Studio является именно UI средой разработки и не предназначена для работы в режиме командной строки. Так, например, для компиляции проектов в системах автоматизации сборок рекомендуется напрямую вызывать сборочную утилиту Microsoft MSBuild, поддерживающую все стандартные типы проектов Visual Studio.

С осторожностью следует применять вызов команд Visual Studio через /command при работе в неинтерактивном режиме рабочего стола (например, при запуске из службы Windows). Например, проверяя возможность интегрировать статический анализ PVS-Studio в сборочные процессы Microsoft Team Foundation, мы столкнулись с несколькими интересными моментами, т.к. по умолчанию Team Foundation работает именно как Windows служба. Сам наш плагин был не готов работать в неинтерактивном режиме рабочего стола, неправильно управляя своими дочерними окнами и диалогами, что в свою очередь приводило к аварийному падению. У Visual Studio таких проблем не обнаружилось, а точнее практически не обнаружилось. После первого запуска среды, для каждого пользователя Visual Studio выдаёт диалог, предлагающий выбрать одну из стандартных конфигураций интерфейса. Этот же диалог она выдала и для пользователя LocalSystem, которому принадлежал сервис Team Foundation. Оказалось, что данный диалог Visual Studio генерирует и в неинтерактивном режиме при вызове /command, блокируя всё дальнейшее исполнение. А так как пользователь не имеет интерактивного рабочего стола, то и закрыть этот диалог оказалось затруднительно. В итоге мы всё же смогли это сделать, запустив Visual Studio для LocalSystem в интерактивном режиме с помощью утилиты psexec из набора PSTools.

Создание и обработка команд в VSPackage. Vsct файлы

Для создания и управления интегрируемыми IDE командами в VSPackage используются таблицы команд (Visual Studio Command Table, vsct файлы). Таблицы команд — это текстовые конфигурационные файлы в формате XML, компилируемые VSCT-компилятором в бинарные cto-файлы (command table output). CTO файлы включаются затем в качестве ресурса в финальную сборку модуля-расширения IDE. С помощью VCST команды могут быть назначены на пункты главного меню IDE или кнопки панелей инструментов. Поддержка VSCT файлов доступна начиная с Visual Studio 2005, в предыдущих версиях IDE для описания команд использовались CTC (command table compiler) файлы, в рамках данной статьи они рассматриваться не будут.

Каждой команде в vsct файле присваивается уникальный идентификатор — CommandID, имя и группа, определяется сочетание для быстрого вызова. С помощью различных флагов задаётся её внешний вид в интерфейсе (в меню или на панели инструментов), определяются параметры её видимости и т.д.

Рассмотрим базовую структуру VSCT файла. Корневой элемент таблицы команд CommandTable должен содержать под-узел Commands, в котором будут определены все пользовательские команды, группы, меню, панели инструментов и т.д. Узел Commands должен также иметь атрибут Package со значением, соответствующим идентификатору разрабатываемого пакета расширения. Под-узел корневого узла Symbols должен содержать определения для всех используемых в VSCT файле идентификаторов. Под-узел корневого узла KeyBindings содержит задаваемые по умолчания сочетания для быстрого вызова команд.

<CommandTable"http://schemas.microsoft.com/VisualStudio/2005-10-
18/CommandTable">

    <Extern href="stdidcmd.h"/>
    <Extern href="vsshlids.h"/>
  <Commands>
    <Groups>
    ...
    </Groups>
    <Bitmaps>
    ...
    </Bitmaps>
  </Commands>
  <Commands package="guidMyPackage">
    <Menus>
    ...
    </Menus>
    <Buttons>
    ...
    </Buttons>
  </Commands>

  <KeyBindings>
    <KeyBinding guid="guidMyPackage" id="cmdidMyCommand1"
 editor="guidVSStd97" key1="221" mod1="Alt" />
  </KeyBindings>
  <Symbols>
    <GuidSymbol name="guidMyPackage" value="{B837A59E-5BF0-4190-B8FC-
FDC35BE5C342}" />
    <GuidSymbol name="guidMyPackageCmdSet" value="{CC8B1E36-FE6B-48C1-
B9A9-2CC0EAB4E71F}">
      <IDSymbol name="cmdidMyCommand1" value="0x0101" />
    </GuidSymbol>
  </Symbols>
</CommandTable>

Элемент Buttons задаёт непосредственно сами IDE команды, задавая их внешний вид и привязывая их к группам команд.

<Button guid="guidMyPackageCmdSet" id="cmdidMyCommand1"
priority="0x0102" type="Button">
  <Parent guid="guidMyPackageCmdSet" id="MyTopLevelMenuGroup" />
  <Icon guid="guidMyPackageCmdSet" id="bmpMyCommand1" />
  <CommandFlag>Pict</CommandFlag>
  <CommandFlag>TextOnly</CommandFlag>
  <CommandFlag>IconAndText</CommandFlag>
  <CommandFlag>DefaultDisabled</CommandFlag>
  <Strings>
    <ButtonText>My &Command 1</ButtonText>
  </Strings>
</Button>

Элемент Menus описывает структуру UI элементов главного меню и панели инструментов, и связывает их с группами команд элемента Groups. Группа команд, связанная с элементом Menu, будет отображена на заданном здесь меню или панели инструментов.

<Menu guid=" guidMyPackageCmdSet" id="SubMenu1" priority="0x0000"
type="Menu">
  <Parent guid="guidMyPackageCmdSet" id="MyTopLevelMenuGroup"/>
  <Strings>
    <ButtonText>Sub Menu 1</ButtonText>
  </Strings>
</Menu>
<Menu guid="guidMyPackageCmdSet" id="MyToolBar1" priority="0x0010"
type="Toolbar">
</Menu>

Элемент Groups формирует группы пользовательских команд среды.

<Group guid="guidMyPackageCmdSet" id="MySubGroup1" priority="0x0020">
  <Parent guid="guidMyPackageCmdSet" id="MyGroup1" />
</Group>

Для добавления vsct файла в MSBuild проект VSPackage необходимо сначала вставить следующий узел для вызова VSCT компилятора в csproj файл (в автогенерируемом проекте SDK шаблона VSPackage vsct файл будет уже добавлен в проект):

<ItemGroup>
  <VSCTCompile Include="TopLevelMenu.vsct">
    <ResourceName>Menus.ctmenu</ResourceName>
  </VSCTCompile>
</ItemGroup>

Для интеграции пользовательской команды или группы команд в одну из стандартных групп Visual Studio, необходимо указать для вашей группы идентификатор целевой стандартной группы в узле parent. Например, для интеграции вашей группы команд в контекстное меню проекта окна Solution Explorer:

<Group guid="guidMyCmdSet" id="ProjectNodeContextMenuGroup" 
  priority="0x07A0">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_PROJNODE" />
</Group>

Как видим, здесь используется стандартный идентификатор IDM_VS_CTXT_PROJNODE. Пройдя по этой ссылке, вы сможете посмотреть список стандартных идентификаторов групп команд Visual Studio.

И затем указать на него с помощью атрибута ProvideMenuResource у вашего наследника класса Package:

[ProvideMenuResource("Menus.ctmenu", 1)]
...
public sealed class MyPackage : Package

Назначение обработчиков для команд, определённых в VSCT файле, возможно с помощью службы, доступной через интерфейс IMenuCommandService. Получить ссылку на него можно с помощью метода GetService класса Package:

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

Приведём пример назначения обработчика на команду меню (команда должна быть определена в vsct файле заранее):

EventHandler eh = new EventHandler(CMDHandler);
CommandID menuCommandID = new CommandID(guidCommand1CmdSet, id); 
//ID and GUID should be the same as in the VCST file
OleMenuCommand menuItem = new OleMenuCommand(eh, menuCommandID);
menuItem.ParametersDescription = "$";
MCS.AddCommand(menuItem);

Для получения аргументов команды во время обработки её вызова можно преобразовать получаемое обработчиком значение EventArgs в OleMenuCmdEventArgs:

void CMDHandler(object sender, EventArgs e)
{                 
  OleMenuCmdEventArgs eventArgs = (OleMenuCmdEventArgs)e;
                   if (eventArgs.InValue != null)
                       param = eventArgs.InValue.ToString();
  ...
}

Работа с командами через интерфейс EnvDTE.DTE

Объект автоматизации EnvDTE.DTE также предоставляет возможности для прямой программной манипуляции (создание, модификация, исполнение) команд через интерфейс dte.Commands и метод dte.ExecuteCommand.

Использование объектной модели автоматизации для вызова, модификации и создания IDE команд, в отличие от механизма VSCT, доступного только для VSPackage, позволяет взаимодействовать с командами из модулей-расширений Visual Studio типа Add-In.

Объект автоматизации DTE позволяет напрямую создавать, модифицировать и вызывать команды через интерфейс DTE.Commands. Метод Commands.AddNamedCommand позволяет добавить команду в IDE (только для Add-In модуля):

dte.Commands.AddNamedCommand(add_in, "MyCommand", "My Command", 
  "My Tooltip", true);

При этом добавленная подобным образом команда будет сохранена в IDE и появится в меню после перезапуска среды, даже если создавшее её расширение не будет загружено. Поэтому вызывать данный метод стоит только при самом первом подключении Add-In модуля после его установки (подробно этот момент описан в разделе, посвященном объектной модели автоматизации Visual Studio). Для метода OnConnection Add-In модулей доступен вариант первичной загрузки (при самой первой инициализации), вызываемый только один раз за всю жизнь модуля, который также можно использовать для интеграции UI элементов в IDE.

public void OnConnection(object application, 
                         ext_ConnectMode connectMode, 
                         object addInInst, ref Array custom)
{
  switch(connectMode)
  {
      case ext_ConnectMode.ext_cm_UISetup:
          ...
          break;

      ...
  }

}

Интерфейс EnvDTE.Command абстрагирует в себе отдельную команду IDE. Его можно использовать для модификации связанной с ним команды. Данный интерфейс позволяет работать с командами среды как из VSPackage, так и из Add-In модулей. Получим ссылку на объект автоматизации EnvDTE.Command для нашей пользовательской команды MyCommand1 и используем данный интерфейс для назначения ей "горячей клавиши" быстрого вызова:

EnvDTE.Command MyCommand1 = 
MyPackage.DTE.Commands.Item("MyGroup.MyCommand1", -1);
MyCommand1.Bindings = new object[1] { "Global::Alt+1" };

Назначенное для команды MyGroup.MyCommand1 сочетание быстрого вызова теперь будет видно в настройках среды в диалоге Keyboard, Environment.

Стоит помнить, что команда Visual Studio не является по умолчанию элементом интерфейса IDE. Метод интерфейса Commands.AddCommandBar позволяет создавать такие элементы UI среды, как пункты главного меню, инструментальные панели, контекстные меню, и назначать на них пользовательские команды.

CommandBar MyToolbar = dte.Commands.AddCommandBar("MyToolbar1", 
  vsCommandBarType.vsCommandBarTypeToolbar) as CommandBar;
CommandBar MyMenu = dte.Commands.AddCommandBar("MyMenu1", 
  vsCommandBarType.vsCommandBarTypeMenu) as CommandBar;
CommandBarButton MyButton1 = MyCommand1.AddControl(MyToolbar) as 
  CommandBarButton;
MyButton1.Caption = "My Command 1";

Для удаления команды или командной панели из IDE можно воспользоваться методом Delete интерфейсов Command/ CommandBar:

MyCommand1.Delete();

В общем случае не рекомендуется создавать команды каждый раз при загрузке Add-In модуля и удалять их при его выгрузке, т.к. это может замедлить загрузку самой IDE, а в случае некорректно отработавшего метода OnDisconnect пользовательские команды не будут полностью очищены до следующего запуска модуля. Поэтому, добавление и удаление команд среды рекомендуется осуществлять в процессе инсталляции/деинсталляции модуля, используя, например, получение ссылки на интерфейс DTE из стороннего приложения, в данном случае инсталлятора. Подробно инициализация Add-Inn модулей и получение доступа к объекту DTE из внешних приложений описаны в разделе, посвящённом модели объектной автоматизации EnvDTE.

Любая команда среды (как пользовательская, так и встроенная) может быть вызвана с помощью с помощью метода ExecuteCommand. Приведём пример вызова пользовательской команды MyCommand1:

MyPackage.DTE.ExecuteCommand("MyGroup.MyCommand1", args);

Для обработки команды основной класс Add-In модуля должен наследоваться от интерфейса IDTCommandTarget и реализовывать метод обработки Exec:

public void Exec(string commandName, 
  vsCommandExecOption executeOption, ref object varIn, 
  ref object varOut, ref bool handled)
{
  handled = false;
  if(executeOption == 
    vsCommandExecOption.vsCommandExecOptionDoDefault)
  {
    if(commandName == "MyAddin1.Connect.MyCommand1")
    {
      ...
      handled = true;
      return;
    }
  }
}

Рекомендуемые ссылки

Инструментальные окна Visual Studio

В данном разделе рассматривается расширение Visual Studio через интеграцию в среду пользовательского инструментального окна. Будут затронуты вопросы регистрации и инициализации пользовательских окон в модулях вида VSPackage и Add-In, отображения в окне пользовательских компонентов, обработки событий и контроля состояния окна.

Введение

Инструментальные окна (tool window) — дочерние окна MDI (Multiple Document Interface) интерфейса Visual Studio предназначенные для вывода информации. Solution Explorer и Error List являются инструментальными окнами. Обычно содержимое инструментального окна не связывается с файлом и не содержит редакторов, для этого существуют специальные окна документов.

Модуль-расширение PVS-Studio интегрирует в IDE несколько инструментальных окон, главным из которых является окно вывода результатов анализа (PVS-Studio Output Window). Из него уже можно открыть другие окна, например окно поиска по списку. Окно вывода результатов PVS-Studio доступно из главного меню Visual Studio (PVS-Studio -> Show PVS-Studio Output Window) и автоматически открывается при запуске анализа.

В большинстве случаев IDE создаёт и использует только один экземпляр каждого инструментального окна (single instance window), причём экземпляр этого окна остаётся открытым до конца сеанса работы среды. При нажатии на кнопку закрытия такое окно скрывается, а при последующем обращении к нему оно вновь становится видимым, причём все отражённые в нём данные сохраняются. Однако создание и интеграция в IDE Multi-Instance пользовательских окон (т.е. окон, которые можно открывать по нескольку раз) также возможны. Инструментальное окно может быть закреплено за определённым контекстом интерфейса IDE (т.н. динамические окна). Такое окно будет автоматически показано пользователю при его попадании в данный контекст.

Интеграция инструментальных окон в IDE поддерживается в VSPackage и Add-In модулях расширений (причём реализации для этих видов модулей различаются), и требует задания их первоначальных параметров и регистрации в системном реестре.

Регистрация и инициализация инструментальных окон

Устанавливаемый вместе с Visual Studio SDK шаблон проекта VSPackage позволяет сгенерировать пользовательское инструментальное окно для создаваемого им проекта пакета модуля-расширения. Этот проект уже должен содержать все рассмотренные ниже элементы, поэтому его удобно использовать как образец при знакомстве с процессом интеграции пользовательских окон в Visual Studio.

Регистрация, инициализация и вызов окна в VSPackage

Регистрация пользовательского окна в среде требует добавления информации о нём в специальный раздел системного реестра ветки Visual Studio. Этот процесс может быть автоматизирован с помощью генерации pkgdef файла, который будет содержать в себе всю необходимую регистрационную информацию об окне. Содержимое pkgdef файла задаётся с помощью специальных регистрационных атрибутов подкласса Package.

За непосредственную регистрацию пользовательского инструментального окна в модуль VSPackage отвечает атрибут ProvideToolWindow класса Package:

[ProvideToolWindow(typeof(MyWindowPane), Orientation = 
ToolWindowOrientation.Right, Style = VsDockStyle.Tabbed, Window = 
Microsoft.VisualStudio.Shell.Interop.ToolWindowGuids.Outputwindow, 
MultiInstances = false, Transient = true, Width = 500, Height = 250, 
PositionX = 300, PositionY = 300)]

Рассмотрим некоторые из параметров данного атрибута. Typeof указывает на пользовательскую реализацию клиентской области окна (ToolWindowPane). Параметр MultiInstances позволяет использовать окно в Multi-Instance режиме, т.е. с возможностью открывать несколько экземпляров окна одновременно. Параметры Orientaton, Size и Style позволяют задать первоначальное положение окна при первом открытии. Стоит помнить, что после первого открытия окна IDE будет сохранять его положение при дальнейших запусках среды. Transient определяет, будет ли окно открыто сразу после загрузки среды Visual Studio в случае, если оно уже было открыто в предыдущем сеансе IDE.

Стоит заметить, что инициализация пользовательского окна в VSPackage (что будет описано чуть позднее) не обязательно совпадает с инициализацией самого подкласса Package, для которого мы и указываем данный регистрационный атрибут. Например, после реализации инструментального окна для плагина PVS-Studio мы сталкивались с ситуацией, при которой окно оказывалось открытым (но не активным) среди нижних вкладок окон среды сразу после запуска Visual Studio, даже несмотря на то, что в атрибут ProvideToolWindow было передано Transient = true. При этом, несмотря на то, что сам плагин проходил инициализацию всегда на старте IDE, само окно оказывалось не до конца инициализированным вплоть до первого к нему обращения, что в частности было видно по неподгруженной иконке на вкладке.

Для окна может быть задана динамическая область видимости с помощью атрибута ProvideToolWindowVisibility, например:

[ProvideToolWindowVisibility(typeof(MyWindowPane), 
/*UICONTEXT_SolutionExists*/"f1536ef8-92ec-443c-9ed7-fdadf150da82")]

В данном примере окно должно быть автоматически открыто при вхождении в UI контекст "Solution Exists".Заметьте, что для каждого пользовательского окна необходимо указывать отдельный атрибут, передавая его тип в качестве первого аргумента.

Для создания и отображения инструментального окна из VSPackage можно воспользоваться методом FindToolWindow класса Package. Данный метод возвращает ссылку на указанный toolwindow объект, создавая его в случае необходимости (при первом запуске single-instance окна). Пример вызова и отображения single-instance окна:

private void ShowMyWindow(object sender, EventArgs e)
{
  ToolWindowPane MyWindow = this.FindToolWindow(typeof(MyToolWindow), 
    0, true);
  if ((null == MyWindow) || (null == MyWindow.Frame))
  {
    throw new NotSupportedException(Resources.CanNotCreateWindow);
  }
  IVsWindowFrame windowFrame = (IVsWindowFrame) MyWindow.Frame;
  ErrorHandler.ThrowOnFailure(windowFrame.Show());
}

В приведённом примере окно будет либо создано при первом вызове, либо показано, если оно было ранее скрыто. При этом третий параметр метода FindToolWindow типа bool определяет, будет ли создан новый экземпляр окна в случае, если он не был найден.

Для создания Multi-Instance инструментальных окон можно использовать метод CreateToolWindow, позволяющий создать окно с заданным идентификатором. Тогда метод вызова toolwindow будет выглядеть следующим образом:

private void CreateMyWindow(object sender, EventArgs e)
{
  for (int i = 0; ; i++)
  {
    // Find existing windows.
    var currentWindow = 
      this.FindToolWindow(typeof(MyToolWindow), i, false);
    if (currentWindow == null)
    {
      // Create the window with the first free ID.
      var window = 
       (ToolWindowPane)this.CreateToolWindow(typeof(MyToolWindow), i);

      if ((null == window) || (null == window.Frame))
      {
        throw new 
          NotSupportedException(Resources.CanNotCreateWindow);
      }
      IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame;

      ErrorHandler.ThrowOnFailure(windowFrame.Show());
      break;
    }
  }
}

Обратите внимание, что теперь в метод FindToolWindow в качестве 3-его аргумента передаётся false, т.е. мы ищем свободный индекс, не инициализируя новые экземпляры окна.

При работе с single-instance окном, после первого вызова среда будет запоминать его последующие положение и размеры, сохраняя их каждый раз перед закрытием. Однако если по какой-либо причине всё же потребуется вручную задать размер и положение окна, это можно сделать с помощью метода SetFramePos класса IVsWindowFrame:

Guid gd = Guid.Empty;
windowFrame.SetFramePos(VSSETFRAMEPOS.SFP_fDockBottom, ref gd, 20, 20
  200, 200);

Стоит помнить, что вызов метода SetFramePos() должен выполняться после вызова метода Show().

Создание и вызов окна в Add-In

Для модуля типа Add-In инициализировать пользовательское инструментальное окно можно с помощью методов EndDTE интерфейса Window2:

public void OnConnection(object application, 
ext_ConnectMode connectMode, object addInInst, ref Array custom)
{
  _applicationObject = (DTE2)application;
  _addInInstance = (AddIn)addInInst;
  EnvDTE80.Windows2 window;
  AddIn add_in;
  object ctlobj = null;
  Window myWindow;

  // Get the window object
  add_in = _applicationObject.AddIns.Item(1);
  window = (Windows2)_applicationObject.Windows;

  // This section specifies the path and class name of the windows 
  // control that you want to host in the new tool window, as well as
  // its caption and a unique GUID.
  string assemblypath = "C:\\MyToolwindow\\MyToolWindowControl.dll";
  string classname = " MyToolWindowControl.MyUserControl";
  string guidpos = "{E87F0FC8-5330-442C-AF56-4F42B5F1AD11}";
  string caption = "My Window";

  // Creates the new tool window and inserts the user control into it.
  myWindow = window.CreateToolWindow2(add_in, assemblypath, 
  classname, caption, guidpos, ref ctlobj);
  myWindow.Visible = true;
}

В приведённом примере создаётся пользовательское инструментальное окно, использующее класс MyToolWindowControl.MyUserControl в качестве клиентской области. Класс MyToolWindowControl.MyUserControl может находиться либо в той же assembly, что и инициализирующий его add-in, либо в отдельной библиотеке, имеющей полную COM видимость (например, через опцию Register for COM interop в настройках проекта). В качестве MyUserControl может быть использован стандартный композитный пользовательский подклас UserControl.

Реализация пользовательского инструментального окна в модуле VSPackage

Инструментальные окна состоят из каркаса-рамки клиентской области. При этом каркас окна предоставляется средой и отвечает за стыковку с другими компонентами интерфейса (docking), размер и положение окна. Клиентская область (pane) отображает содержимое окна, контролируемое пользователем. Инструментальные окна могут содержать пользовательские WinForms и WPF компоненты и предоставляют возможность обрабатывать такие стандартные события, как например OnShow, OnMove и т.п.

Пользовательское инструментальное окно, а точнее его клиентская область, реализуется путём наследования от класса, реализующего стандартное пустое окно IDE — ToolWindowPane:

[Guid("870ab1d8-b434-4e86-a479-e49b3c6797f0")]
public class MyToolWindow : ToolWindowPane
{

  public MyToolWindow():base(null)
  {
    this.Caption = Resources.ToolWindowTitle;
    this.BitmapResourceID = 301;
    this.BitmapIndex = 1;
    ...

  }

}

Атрибут Guid уникально идентифицирует каждое пользовательское окно. В случае если модуль создаёт несколько окон разного типа, каждое из них должно иметь свой уникальный Guid. Подкласс ToolWindowPane может в дальнейшем быть модифицирован для отображения в нём пользовательских компонентов и управления его состоянием.

Хостинг пользовательских компонентов

Базовый класс ToolWindowPane реализует пустое инструментальное окно среды. Наследование от данного класса позволяет отобразить в этом окне пользовательские WinForms или WPF компоненты.

До версии Visual Studio 2008 инструментальные окна нативно поддерживали отображение WinForms пользовательских компонентов, а также могли отображать WPF компоненты с помощью WPF Interoperability компонента ElementHost. Начиная с Visual Studio 2010, инструментальные окна стали основываться на технологии WPF, но всё еще поддерживают загрузку и отображение WinForms компонентов в режиме совместимости.

Для отображения в окне пользовательского WinForms компонента можно воспользоваться переопределённым свойством Window у ToolWindowPane:

public MyUserControl control;

public MyToolWindow():base(null)
{
  this.Caption = Resources.ToolWindowTitle;
  this.BitmapResourceID = 301;
  this.BitmapIndex = 1;
  this.control = new MyUserControl();
}

public override IWin32Window Window
{
  get { return (IWin32Window)control; }
}

При этом MyUserControl представляет собой обычный составной компонент типа System.Windows.Forms.UserControl, позволяющий отобразить любые другие пользовательские компоненты. Для отображения WPF компонентов в нём можно воспользоваться стандартным компонентом WPF ElementHost.

Начиная с версии Visual Studio 2010 появилась возможность нативно отображать WPF компоненты. Для этого нужно передать ссылку на ваш WPF компонент полю Content базового класса:

public MyToolWindow():base(null)
{
  this.Caption = Resources.ToolWindowTitle;
  this.BitmapResourceID = 301;
  this.BitmapIndex = 1;
  base.Content = new MyWPFUserControl();
}

Заметьте, что одновременное использование этих двух методов не поддерживается. При назначении WPF компонента в base.Content переопределённое свойство Window будет проигнорировано.

В основном окне Output Window модуля-расширения PVS-Studio расположена виртуальная таблица, основанная на open-source проекте SourceGrid, предназначенная для работы с результатами статического анализа. При этом таблица используется для отображения стандартной таблицы ADO.NET System.Data.Datatable, которая и используется для непосредственного хранения результатов работы анализатора. До версии 4.00 PVS-Studio использовал для вывода результатов анализа стандартное IDE окно Error List, однако, по мере развития возможностей анализатора, его функционала стало недостаточно. Помимо невозможности быть расширенным такими специфичными для статического анализатора элементами управления, как например механизмы для фильтрации и средства подавления ложных срабатываний, Error List, являясь обычным real grid элементом, позволял адекватно оперировать лишь 1-2 тысячами сообщений. Большее количество сообщений начинало приводить уже к заметным лагам всего интерфейса IDE. Практика же использования статического анализа показала, что для крупных проектов, как например Chromium или LLVM, общее количество диагностических сообщений (с учётом уже размеченных ложных срабатываний, пользовательских низкоуровневых и оптимизационных диагностик) вполне может достигать значений в несколько десятков тысяч.

Реализация собственного окна вывода результатов PVS-Studio на основе виртуального грида, связанного с таблицей БД, позволяет отображать и удобно работать с сотнями тысяч сообщений одновременно. Очень важным аспектом при работе с результатами статического анализа является также возможность их гибкой фильтрации, так как ручной просмотр для поиска реальных ошибок даже такого "небольшого" количество диагностических сообщений как 1-2 тысячи, практически невозможен. При хранении же результатов в таблице Datatable подобная фильтрация легко доступна с помощью простых SQL запросов, причём результаты наложения фильтров становятся видны практически моментально в отражении таблицы на виртуальном гриде.

Обработка событий в инструментальных окнах

Клиентская область инструментального окна (представленная нашим наследником от класса ToolWindowPane) может обрабатывать события взаимодействия пользователя с окном IDE. Для того чтобы подписаться на обработку этих событий можно воспользоваться интерфейсом IVsWindowFrameNotify3. Приведём пример реализации данного интерфейса:

public sealed class WindowStatus: IVsWindowFrameNotify3
{
  // Private fields to keep track of the last known state
  private int x = 0;
  private int y = 0;
  private int width = 0;
  private int height = 0;
  private bool dockable = false;

  #region Public properties
            
  // Return the current horizontal position of the window
  public int X
  {
    get { return x; }
  }
            
  // Return the current vertical position of the window
  public int Y
  {
    get { return y; }
  }

  // Return the current width of the window
  public int Width
  {
    get { return width; }
  }

  // Return the current height of the window
  public int Height
  {
    get { return height; }
  }
            
  // Is the window dockable
  public bool IsDockable
  {
    get { return dockable; }
  }

  #endregion

  public WindowStatus()
  {}

  #region IVsWindowFrameNotify3 Members
  // This is called when the window is being closed
  public int OnClose(ref uint pgrfSaveOptions)
  {
    return Microsoft.VisualStudio.VSConstants.S_OK;
  }

  // This is called when a window "dock state" changes. 
  public int OnDockableChange(int fDockable, int x, int y, int w, 
  int h)
  {
    this.x = x;
    this.y = y;
    this.width = w;
    this.height = h;
    this.dockable = (fDockable != 0);
                
    return Microsoft.VisualStudio.VSConstants.S_OK;
  }

  // This is called when the window is moved
  public int OnMove(int x, int y, int w, int h)
  {
    this.x = x;
    this.y = y;
    this.width = w;
    this.height = h;

    return Microsoft.VisualStudio.VSConstants.S_OK;
  }

  // This is called when the window is shown or hidden
  public int OnShow(int fShow)
  {
    return Microsoft.VisualStudio.VSConstants.S_OK;
  }

  /// This is called when the window is resized
  public int OnSize(int x, int y, int w, int h)
  {
    this.x = x;
    this.y = y;
    this.width = w;
    this.height = h;
    return Microsoft.VisualStudio.VSConstants.S_OK;
  }

  #endregion

}

Как видно из приведённого кода, реализующий интерфейс класс WindowStatus может обрабатывать такие изменения в состоянии пользовательского окна, как изменение размера, положения на экране, видимости и т.п. Теперь подпишем наше окно на обработку данных событий. Для этого переопределим метод OnToolWindowCreated у нашего класса-наследника ToolWindowPane:

public class MyToolWindow: ToolWindowPane
{
  public override void OnToolWindowCreated()
  {
    base.OnToolWindowCreated();

    // Register to the window events
    WindowStatus windowFrameEventsHandler = new WindowStatus();

ErrorHandler.ThrowOnFailure(((IVsWindowFrame)this.Frame).SetProperty(
  (int)__VSFPROPID.VSFPROPID_ViewHelper,
  (IVsWindowFrameNotify3)windowFrameEventsHandler));
  }

  ...
}

Контроль состояния окна

Контроль состояния окна можно осуществлять с помощью обработчиков событий нашей реализации интерфейса IVsWindowFrameNotify3.

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

Метод OnClose сообщает о закрытии каркаса окна, позволяя задать необходимое поведение IDE с помощью параметра pgrfSaveOptions, управляющего отображением диалога о сохранении открытого в окне документа (__FRAMECLOSE).

Метод OnDockableChange информирует модуль об изменении docking статуса окна. Параметр fDockable показывает, сцеплено ли окно с каким-либо другим, а остальные параметры указывают новый размер и координаты окна до или после сцепления.

Параметры методов OnMove и OnSize сообщают новые координаты и/или размер окна при его перетаскивании и маштабировании.

Рекомендуемые ссылки

Интеграция в настройки Visual Studio

В данном разделе рассматривается расширение среды Visual Studio путём интеграции в её настройки пользовательских групп и страниц настроек. Показана регистрация и интеграция в IDE пользовательских страниц настроек с помощью различных видов подключаемых модулей расширений, способы отображения стандартных и пользовательских компонентов в них. Рассмотрены возможности программного доступа к настройкам среды через объектную модель автоматизации Visual Studio, а также механизм сохранения состояния настроек.

Введение

Visual Studio использует единое диалоговое окно для доступа к настройкам различных компонентов среды разработки. Это окно доступно через пункт главного меню IDE Tools -> Options. Базовым элементом настроек Visual Studio является страница настроек. Диалоговое окно Options располагает страницы настроек в древовидной структуре в соответствии с их принадлежностью к различным функциональным группам. Причём каждая из страниц может быть уникально идентифицирована по имени её группы и персональному имени, например страница настроек редактора Visual Basic кода: "Text Editor, Basic".

Модули-расширения имеют возможность читать и модифицировать значения отдельных настроек у зарегистрированных в IDE страниц. Также модули могут создавать и регистрировать собственные пользовательские страницы в среде с использованием объектной модели автоматизации и классов MPF (Managed Package Framework, доступно только для VSPackage модулей). Visual Studio имеет встроенный механизм для сохранения состояния объекта-страницы, который используется по умолчанию, а также может быть переопределён или отключен.

Создание и регистрация пользовательских страниц настроек

При разработке модуля-расширения Visual Studio может оказаться полезным сопоставить его с одной или несколькими пользовательскими страницами настроек в меню Tools -> Options. Подобный инструмент для конфигурации и управления расширением будет соответствовать UI парадигме среды разработки и удобен при работе с расширением непосредственно из IDE. Методы реализации пользовательской страницы настроек, её интеграции в IDE и регистрации будут различаться в зависимости от типа разрабатываемого модуля-расширения и используемой технологии (модель автоматизации или MPF).

Интеграция с использованием MPF классов

Managed Package Framework позволяет создавать пользовательские страниц настроек путём наследования от класса DialogPage. В связи с тем, что среда независимо подгружает страницу настроек при открытии соответствующего раздела в диалоге Tools -> Options, каждая пользовательская страница должна быть реализована в виде отдельного независимого объекта.

Объект, реализующий таким образом пользовательскую страницу настроек, должен быть связан с пакетом расширения Visual Studio (VSPackage) через атрибут ProvideOptionPage подкласса Package.

[ProvideOptionPageAttribute(typeof(OptionsPageRegistration),
"MyPackage", "MyOptionsPage", 113, 114, true)]

Атрибут задаёт группу и имя страницы в диалоге настроек IDE и должен быть определён для каждой интегрируемой пользовательской страницы. Фактически атрибут обеспечивает регистрацию страницы в системном реестре через pkgdef файл, и никак сам по себе не влияет на исполняемый код. Для корректной работы страница настроек должна быть зарегистрирована в следующем разделе системного реестра:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\<VsVersion>\
ToolsOptionsPages

Здесь <VsVersion> - номер версии Visual Studio, например 10.0. Данная запись будет автоматически создана при использовании атрибута ProvideOptionPage. Стоит также помнить, что для удаления пакета-расширения из Visual Studio потребуется очистить все созданные им записи в системном реестре, в том числе и записи, относящиеся к пользовательским страницам настроек. Начиная с версии Visual Studio 2010 VSPackage модули могут использовать VSIX пакеты для своего развёртывания и деинсталляции, при этом инсталлятор VSIX автоматически записывает или удаляет записи о модуле в реестре. Для более старых версий IDE очистку реестра потребуется провести вручную, например в standalone инсталляторе.

Шестой bool аргумент конструктора атрибута позволяет зарегистрировать пользовательскую страницу как объект автоматизации. Это позволит получить доступ к настройкам, определённым в данной странице через интерфейсы EnvDTE, из сторонних plug-in модулей. Регистрация объекта автоматизации требует создания записей в системном реестре (что происходит автоматически при использовании данных атрибутов) в следующих ветках:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\<Version\Packages\
<PackageGUID>\Automation 

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\<Version>\
AutomationProperties

Атрибут ProvideProfile позволяет зарегистрировать страницу настроек или любой другой независимый объект, реализующий интерфейс IProfileManager, для использования встроенного IDE механизм сохранения настроек.

Реализация наследования от MPF класса DialogPage

Минимальным требованием к подклассу DialogPage для реализации пользовательской страницы настроек является наличие в классе-наследнике открытых свойств (public properties). Подобная базовая реализация будет выглядеть следующим образом:

namespace MyPackage
{
  class MyOptionsPage : DialogPage
  {
    bool myOption = true;
    public bool MyOption
    {
        get { return this. myOption; }
        set { this. myOption = value; }
    }
  }
}

Данная базовая реализация подкласса DialogPage будет использовать для отображения клиентской области пользовательского окна настроек стандартный PropertyGrid, в котором будут показаны все открытые свойства класса-наследника. Это может оказаться полезным, когда конфигурационные параметры плагина достаточно просты для их отображения и редактирования через стандартные PropertyGrid редакторы. Использование родного средства отображения среды позволит также избежать и распространённых проблем с корректным отображением и масштабированием пользовательских интерфейсных компонентов (например, на различных системных DPI) непосредственно в самом диалоговом окне Visual Studio.

Для отображения в окне настроек пользовательского интерфейса необходимо переопределить свойство Window класса DialogPage:

[BrowsableAttribute(false)]
protected override IWin32Window Window
{
  get
  {
    return MyUserControl;
  }
}

В него необходимо передать ссылку на пользовательский объект IWin32Window, реализующий интерфейс клиентской области окна. Также стоит помнить, что Visual Studio требует от таких окон быть постоянными, т.е. они не должны пересоздаваться в последующих вызовах. Т.к. такие объекты, как Windows Forms, могут в течение своего существования самостоятельно пересоздавать свой оконный handle, желательно использовать здесь ссылку, полученную через объект типа UserControl.

Через свойство AutomationObject пользовательской страницы настроек, унаследованной от класса DialogPage, определяются те открытые свойства, которые будут отображены через стандартные компоненты в окне настроек и к которым будет применён стандартный IDE механизм сохранения состояния. По умолчанию AutomationObject возвращает ссылку на сам подкласс DialogPage, но если он вернёт ссылку на какой-либо другой объект, то открытые свойства именно этого объекта будут использованы для отображения и сохранения состояния настроек. В стандартной реализации системный реестр используется как локальное хранилище настроек. Переопределение метода подкласса DialogPage.SaveSettingsToStorage позволяет изменить способ работы механизма сохранения состояния настроек среды (по аналогии, переопределение метода LoadSettingsFromStorage позволяет изменить работу механизма восстановления настроек).

public override void SaveSettingsToStorage() { ... }

Зарегистрированные как объекты автоматизации страницы могут сохранять своё состояние, по аналогии с сохранением состояния всех остальных страниц настроек, во внешнем XML файле при использовании команд среды Tools -> Import/Export Settings через стандартную реализацию метода SaveSettingsToXml, который при необходимости также может быть переопределён.

Заметим, что интеграция в диалог настроек Visual Studio не является единственным и обязательным методом создания интерфейса для конфигурации IDE плагина. И если стандартного PropertyGrid компонента оказывается недостаточно для управления настройками, а встроенный механизм сохранения настроек по каким-то причинам не планируется использовать, то вполне разумным может оказаться создание полностью независимого от среды диалога. К преимуществам подобного подхода можно отнести высокую переносимость данного решения (например, плагин имеет несколько версий для различных IDE с едиными настройками), а также полный контроль непосредственно над самим диалоговым окном, что в свою очередь существенно облегчает поддержку различных пользовательских конфигураций по разрешению, DPI и т.п. Однако стоит помнить, что настройки из такого диалогового окна становятся недоступными для других разработчиков через объектную модель автоматизации.

В модуле-расширении PVS-Studio используется собственный механизм сохранения состояния настроек через внешний xml-файл, а настройки, интегрируемые в IDE, используются только для отображения\модификации части из этих внутренних настроек плагина. Наличие встроенного механизма сохранения состояния у Visual Studio очень часто приводило к конфликтам с внутренними настройками PVS-Studio, вызывая десинхронизацию. Поэтому, использование в модуле-расширении независимого механизма настроек также может потребовать переопределения стандартных механизмов Visual Studio (например, отключением их через пустые методы) для исключения подобных конфликтов.

Интеграция через xml описание Add-In модуля

Пользовательская страница настроек может быть интегрирована в IDE с помощью независимого XML файла описания AddIn. При этом содержимое интегрируемой пользовательской страницы должно быть реализовано в виде пользовательского компонента, например System.Windows.Forms.UserControl. Данный компонент никак не связан непосредственно с самим Add-In модулем, и поэтому может быть реализован как в самой библиотеке модуля, так и в виде отдельной независимой библиотеки. Возможно создание addin файла, описывающего только такой независимый пользовательский компонент. Приведём пример xml файла описания Add-In модуля, содержащего определение для пользовательской страницы настроек.

<?xml version="1.0" encoding="UTF-16" standalone="no"?>
<Extensibility 
xmlns="http://schemas.microsoft.com/AutomationExtensibility">
  <HostApplication>
    <Name>Microsoft Visual Studio Macros</Name>
    <Version>10.0</Version>
  </HostApplication>
  <HostApplication>
    <Name>Microsoft Visual Studio</Name>
    <Version>10.0</Version>
  </HostApplication>
  <Addin>
    <FriendlyName>My Add in</FriendlyName>
    <Description>My Addin 1</Description>
    <Assembly>c:\MyAddIn1\MyAddin1.dll</Assembly>
    <FullClassName>MyAddin1.Connect</FullClassName>
    <LoadBehavior>0</LoadBehavior>
    <CommandPreload>1</CommandPreload>
    <CommandLineSafe>0</CommandLineSafe>
  </Addin>
  <ToolsOptionsPage>
    <Category Name="MyAddIn1">
      <SubCategory Name="My Tools Options Page">
      <Assembly> c:\MyAddIn1\MyAddin1.dll</Assembly>
      <FullClassName>MyAddin1.UserControl1</FullClassName>
      </SubCategory>
    </Category>
  </ToolsOptionsPage>
</Extensibility>

Описание пользовательской страницы настроек расположено в узле <ToolsOptionsPage>. Здесь узел <Assembly> указывает на библиотеку, содержащую пользовательский компонент, который будет использован для отображения клиентской области окна натроек. Узел <FullClassName> указывает на полное имя пользовательского компонента в формате Namespace.ClassName. Узлы <Category> и <SubCategory> определяют положение пользовательской страницы в древовидной структуре настроек диалога Tools -> Options, задавая группу и персональное имя страницы. В качестве <Category> может быть указана как уже существующая, так и новая группа. Как видно из примера, в данном случае пользовательский компонент MyAddin1.UserControl1 находится в одной библиотеке с самим модулем, но это не является обязательным требованием.

Visual Studio подгружает страницу настроек после первого обращения к ней через диалог Options. В отличие от интеграции страницы настроек через Managed Package Framework, описание страницы содержится только в xml файле описании addin, и, соответственно, страница будет загружена только при обнаружении средой такого файла. Visual Studio читает доступные ей addin файлы непосредственно после своей загрузки. Поиск addin файлов производится в директориях, задаваемых на странице настроек Environment -> Add-In/Macross Security. В отличие от страниц настроек, реализованных через MPF, подобный высокоуровневый метод интеграции не регистрирует страницу, как объект автоматизации, и соответственно, не предоставляет возможностей для использования механизмов доступа к её содержимому через объектную модель автоматизации и встроенных IDE механизмов сохранения состояния страницы.

Доступ к окнам настроек с помощью модели автоматизации

Объектная модель автоматизации Visual Studio предоставляет возможность получить доступ ко всем системным настройкам среды диалога Tools -> Options, кроме страниц Dynamic Help и Fonts and Colors (для них существуют отдельные API). Пользовательские страницы настроек будут доступны через модель автоматизации в случае, если они были зарегистрированы как объекты автоматизации, как было описано в предыдущей главе.

Для получения настроек можно воспользоваться методом get_Properties интерфейса EnvDTE.DTE:

Properties propertiesList = PVSStudio.DTE.get_Properties("MyPackage", 
"MyOptionsPage");

Страница настроек здесь идентифицируется по её имени и имени её группы. Для получения конкретного значения свойства:

Property MyProp1 = propertiesList.Item("MyOption1");

Теперь значение свойства можно получить и модифицировать через поле MyProp1.Value.

Для открытия страницы пользовательской настроек в диалоге Options можно воспользоваться методом ShowOptionPage MPF класса Package:

MyPackage.ShowOptionPage(typeof(MyOptionsPage));

Данные метод принимает тип (результат typeof) пользовательского класса наследника от DialogPage. Если необходимо открыть страницу, определённую вне разрабатываемого VSPackage (например, стандартную страницу IDE или страницу, определённую в другом модуле), то можно найти её по GUID идентификатору, который задан в следующей ветке реестра:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\
ToolsOptionsPages\<OptionsPageNme>\

Где <OptionsPageName> — имя страницы в диалоге Tools -> Options. Приведём пример открытия стандартной страницы IDE TextEditor -> General с помощью глобальной службы IMenuCommandService:

string targetGUID = "734A5DE2-DEBA-11d0-A6D0-00C04FB67F6A";
var command = new CommandID(VSConstants.GUID_VSStandardCommandSet97,
  VSConstants.cmdidToolsOptions);
var mcs = GetService(typeof(IMenuCommandService)) 
  as MenuCommandService;
mcs.GlobalInvoke(command, targetGUID);

На самом деле, данный код фактически эквивалентен выполнению команды среды Tools.Options. Её можно также вызвать с помощью метода ExecuteCommand объекта EnvDTE.DTE:

dte.ExecuteCommand("Tools.Options", 
"734A5DE2-DEBA-11d0-A6D0-00C04FB67F6A").

Рекомендуемые ссылки

Проектная модель Visual Studio

В данном разделе будет рассмотрена структура проектной модели Visual Studio и её реализация на примере стандартной модели Visual C++ (VCProject). Приведены примеры использования проектной модели для получения списков проектных элементов и их компиляционных свойств через соответствующие конфигурации. Также будет рассмотрена сторонняя (пользовательская) реализация проектной модели на примере независимой изолированной оболочки (Visual Studio Isolated Shell) Atmel Studio

Введение

Проектная модель Visual Studio представляет собой группу интерфейсов, описывающих функционал компилятора, линковщика и других сборочных инструментов, а также структуру MSVS-совместимых проектов. Она связана с объектной моделью автоматизации через late-bound свойство VCProjects. Проектная модель Visual C++ является расширением стандартной проектной модели Visual Studio, обеспечивая возможность доступа к специфичному для Visual C++ (vcrpoj/vcxproj) проектов функционалу. Проектная модель Visual C++ является самостоятельным COM компонентом, доступным через файл VCProjectEngine.dll, который также может быть использован независимо вне среды Visual Studio. Сторонние разработчики могут создавать свои реализации проектных моделей, добавляя таким образом в Visual Studio поддержку новых компиляторов и языков программирования.

Структура проектной модели

Visual Studio предоставляет расширяемую проектно-независимую объектную модель, в которой представлены решения, проекты, объекты кода, документы и т.п. Каждый тип MSVS проектов представлен соответствующим ему интерфейсом автоматизации. Каждый инструмент в среде, имеющий сопоставленные с ним проекты, также сопоставлен и с объектом типа Project. Стандартная модель Visual C++ также следует данной общей схеме проектной автоматизации:

Projects
  |- Project -- Object(unique for the project type)
      |- ProjectItems (a collection of ProjectItem)
          |- ProjectItem (single object) -- ProjectItems (another
                                                          collection)
              |- Object(unique for the project type)

Интерфейс Projects предоставляет совокупность абстрактных проектов типа Project. Интерфейс Project описывает абстрактный проект, т.е. может указывать на проект любой проектной модели, придерживающейся стандартной схемы. При этом все уникальные свойства конкретной модели должны быть описаны через специальный, уникальный для данной модели, интерфейс. Ссылку на такой объект можно получить через поле Project.Object. Например, для проектной модели Visual C++ такой уникальный объект будет иметь тип VCProject, а для проектной модели Atmel Studio это будет, например, AvrGCCNode:

Project proj;
...
VCProject vcproj = proj.Object as VCProject;
AvrGCCNode AvrGccProject = proj.Object as AvrGCCNode;

Совокупность Projects можно получить для всех загруженных в IDE проектов solution файла через поле dte.Solution.Projects или только для одной проектной модели через метод DTE.GetObject. Например, для проектов Visual C++:

Projects vcprojs = m_dte.GetObject("VCProjects") as Projects;

Для получения проектов всех типов можно воспользоваться следующим кодом:

Projects AllProjs = PVSStudio.DTE.Solution.Projects;

Интерфейс ProjectItems представляет совокупность абстрактных элементов дерева проекта типа ProjectItem. По аналогии с Project, ProjectItem может описывать элемент любого типа, содержащий в том числе и такую же вложенную совокупность ProjectItems (через поле ProjectItem.ProjectItems) или быть проектом Project. Объект уникальный для конкретной проектной модели можно по аналогии получить через ProjectItem.Object. Например, у проектной модели Visual C++ файл с исходным кодом представлен типом VCFile, а у проектной модели Atmel Studio - AvrGccFileNode:

ProjectItem projectItem;
...
VCFile file = projectItem.Object as VCFile;
AvrGccFileNode file = projectItem.Object as AvrGccFileNode;

Аналогично выглядит и получение вложенного проекта, в случае, когда данный элемент иерархии описывает проект:

Project proj = projectItem.Object as Project;

Рекурсивный обход всех элементов ветви Solution дерева

Для обхода ветви Solution дерева можно воспользоваться интерфейсом управления иерархиями IVsHierarchy. Интерфейс предоставляет доступ к абстрактным узлам дерева, каждый из которых может являться листом, контейнером элементов или ссылкой на другую иерархию. Каждый узел дерева уникально идентифицируется через DWORD идентификатор VSITEMID. Такие идентификаторы уникальны в рамках одной иерархии и имеют в ней ограниченный срок существования.

Объект иерархии можно получить для ветки отдельного проекта с помощью метода VsShellUtilities.GetHierarchy:

public static IVsHierarchy ToHierarchy(EnvDTE.Project project)
{
  System.IServiceProvider serviceProvider = 
    new ServiceProvider(project.DTE as
  Microsoft.VisualStudio.OLE.Interop.IServiceProvider);
  Guid guid = GetProjectGuid(serviceProvider, project);
  if (guid == Guid.Empty)
    return null;
  return VsShellUtilities.GetHierarchy(serviceProvider, guid);
}

Здесь иерархия была получена для проекта по его GUID идентификатору. Приведём пример метода GetProjectGuid, позволяющего получить такой идентификатор проекта:

private static Guid GetProjectGuid(System.IServiceProvider 
  serviceProvider, Project project)
{
  if (ProjectUnloaded(project))
    return Guid.Empty;

  IVsSolution solution = 
   (IVsSolution)serviceProvider.GetService(typeof(SVsSolution)) as
     IVsSolution;
  IVsHierarchy hierarchy;
  solution.GetProjectOfUniqueName(project.FullName, out hierarchy);
  if (hierarchy != null)
  {
    Guid projectGuid;

    ErrorHandler.ThrowOnFailure(
      hierarchy.GetGuidProperty(
      VSConstants.VSITEMID_ROOT,
    (int)__VSHPROPID.VSHPROPID_ProjectIDGuid,
      out projectGuid));

    if (projectGuid != null)
    {
      return projectGuid;
    }
  }

  return Guid.Empty;
}

Интерфейс IEnumHierarchies позволяет сразу получить совокупность таких иерархий для проектов определённого, заданного типа, через метод solution. GetProjectEnum. Пример получения иерархий для всех Visual C++ проектов Solution дерева:

IVsSolution solution = PVSStudio._IVsSolution;
if (null != solution)
{
  IEnumHierarchies penum;
  Guid nullGuid = Guid.Empty;
  Guid vsppProjectGuid = 
    new Guid("8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942");

  //You can ask the solution to enumerate projects based on the
  //__VSENUMPROJFLAGS flags passed in. For
  //example if you want to only enumerate C# projects use
  //EPF_MATCHTYPE and pass C# project guid. See
  //Common\IDL\vsshell.idl for more details.
  int hr = solution.GetProjectEnum(
    (uint)(__VSENUMPROJFLAGS.EPF_LOADEDINSOLUTION |   
    __VSENUMPROJFLAGS.EPF_MATCHTYPE),
    ref vsppProjectGuid, out penum);
  ErrorHandler.ThrowOnFailure(hr);
  if ((VSConstants.S_OK == hr) && (penum != null))
  {
    uint fetched;
    IVsHierarchy[] rgelt = new IVsHierarchy[1];
    PatternsForActiveConfigurations.Clear();
    while (penum.Next(1, rgelt, out fetched) == 0 && fetched == 1)
    {
      ...
    }
  }
}

Здесь метод GetProjectEnum позволяет получить иерархии проектов указанного через GUID идентификатор типа. GUID идентификаторы для стандартных типов проектов Visual Studio/MSBuild можно посмотреть по этой ссылке. Метод penum.Next() позволит нам обойти все полученные таким образом проектные иерархии (массив rgelt). Стоит помнить, что пользовательские проектные модели, определяющие новый тип проекта, также могут иметь свой уникальный идентификатор. Идентификаторы для проектов пользовательских типов можно посмотреть непосредственно в самих XML файлах проектов данных моделей.

Практика разработки IDE плагина PVS-Studio, однако, показывает, что возможна и обратная ситуация, т.е. пользовательский тип проекта, использующий GUID одного из стандартных проектных типов, причём обычно тот, от которого он наследовался. В частности, мы столкнулись с типом проектов VCProject, расширенным для разработки под платформу Android. Как результат, данное расширение проектной модели приводило к падению нашего модуля, т.к. оно не предоставляло через API интерфейсы некоторые из присутствующих в VCProject полей (например, поддержка OpenMP). Сложность данной ситуации в том, что такой расширенный проектный тип невозможно дифференцировать от обычных проектов, и соответственно, корректно обработать его. Поэтому при расширении проектной модели через пользовательские типы, для избегания конфликтов с другими (в том числе и пользовательскими) компонентами IDE, стоит помнить о необходимости обеспечения возможности для её уникальной идентификации.

Имея объект иерархии IVsHierarchy проекта, мы можем осуществить рекурсивный обход всех элементов данной ветки Solution дерева с помощью метода hierarchy.GetProperty, позволяющего получить заданные свойства каждого узла иерархии:

EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, MyProjectHierarchy, 
0, true);
  ...
public void EnumHierarchyItemsFlat(uint itemid, IVsHierarchy 
  hierarchy, int recursionLevel, bool visibleNodesOnly)
{
  if (hierarchy == null)
  return;
  int hr; object pVar;

  hr = hierarchy.GetProperty(itemid, 
    (int)__VSHPROPID.VSHPROPID_ExtObject, out pVar);

  ProjectItem projectItem = pVar as ProjectItem;
  if (projectItem != null)
  {
    ...
  }

  recursionLevel++;
  //Get the first child node of the current hierarchy being walked
  hr = hierarchy.GetProperty(itemid,
    (visibleNodesOnly ? (int)__VSHPROPID.VSHPROPID_FirstVisibleChild 
    :(int)__VSHPROPID.VSHPROPID_FirstChild),
    out pVar);
  Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr);
  if (VSConstants.S_OK == hr)
    {
      //We are using Depth first search so at each level we recurse
      //to check if the node has any children
      // and then look for siblings.
      uint childId = GetItemId(pVar);
      while (childId != VSConstants.VSITEMID_NIL)
      {
        EnumHierarchyItemsFlat(childId, hierarchy, recursionLevel, 
        visibleNodesOnly);
        hr = hierarchy.GetProperty(childId,
          (visibleNodesOnly ?
          (int)__VSHPROPID.VSHPROPID_NextVisibleSibling :
          (int)__VSHPROPID.VSHPROPID_NextSibling),
          out pVar);
        if (VSConstants.S_OK == hr)
        {
          childId = GetItemId(pVar);
        }
        else
        {
          Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr);
          break;
        }
      }
    }
  }

  private uint GetItemId(object pvar)
  {
    if (pvar == null) return VSConstants.VSITEMID_NIL;
    if (pvar is int) return (uint)(int)pvar;
    if (pvar is uint) return (uint)pvar;
    if (pvar is short) return (uint)(short)pvar;
    if (pvar is ushort) return (uint)(ushort)pvar;
    if (pvar is long) return (uint)(long)pvar;
    return VSConstants.VSITEMID_NIL;
  }

Полученный в данном примере объект типа ProjectItem для каждого узла дерева позволит нам получить соответствующий ему объект проектной модели Visual С++ (как впрочем и любой другой модели) через его поле Object, как было описано выше.

Обход всех проектов Solution дерева

Для обхода всех проектов дерева можно воспользоваться интерфейсом DTE.Solution.Projects:

if (m_DTE.Solution.Projects != null)
  {
  try
    {
      foreach (object prj in m_DTE.Solution.Projects)
      {
        EnvDTE.Project proj = prj as EnvDTE.Project;
        if (proj != null)
          WalkSolutionFolders(proj);
      } 
    }
  }

Помимо непосредственно проектов, Solution дерево также может содержать узлы-папки (Solution Folders). Их нужно учитывать при обходе каждого Project элемента:

public void WalkSolutionFolders(Project prj)
{
  VCProject vcprj = prj.Object as VCProject;
  if (vcprj != null && prj.Kind.Equals(VCCProjectTypeGUID))
  {
    if (!ProjectExcludedFromBuild(prj))
    {
      IVsHierarchy projectHierarchy = ToHierarchy(prj);
      EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, 
      projectHierarchy, 0, false);
    }
  }
  else if (prj.ProjectItems != null)
  {
    foreach (ProjectItem item in prj.ProjectItems)
    {
      Project nextlevelprj = item.Object as Project;
      if (nextlevelprj != null && !ProjectUnloaded(nextlevelprj))
      WalkSolutionFolders(nextlevelprj);
    }
  }
} 

Отдельную проверку стоит добавить для проектов, исключённых из сборки, т.к. их элементы не будут доступны через модель автоматизации после их выгрузки:

public bool ProjectExcludedFromBuild(Project project)
{
  if (project.UniqueName.Equals("<MiscFiles>", 
    StringComparison.InvariantCultureIgnoreCase))
  return true;
  Solution2 solution = m_DTE.Solution as Solution2;
  SolutionBuild2 solutionBuild = 
    (SolutionBuild2)solution.SolutionBuild;
    SolutionContexts projectContexts = 
    solutionBuild.ActiveConfiguration.SolutionContexts;
    //Skip this  project if it is excluded from build.
    bool shouldbuild = 
      projectContexts.Item(project.UniqueName).ShouldBuild;
    return !shouldbuild;
}

Обход выделенных элементов

Для обхода элементов, выделенных пользователем в интерфейсе окна Solution Explorer, можно воспользоваться интерфейсом DTE.SelectedItems.

foreach (SelectedItem item in items)
{
  VCProject vcproj = null;
  if (item.Project != null)
  {
    vcproj = item.Project.Object as VCProject;

    if (vcproj != null && item.Project.Kind.Equals("{" + 
      VSProjectTypes.VCpp + "}"))
      {
        IVsHierarchy projectHierarchy = ToHierarchy(item.Project);
        PatternsForActiveConfigurations.Clear();
        EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, 
        projectHierarchy, 0, false, files, showProgressDialog);
      }
      else if (item.Project.ProjectItems != null)
      {
        //solution folder
        if (!ProjectUnloaded(item.Project))
          WalkSolutionFolders(item.Project);
      }
    }
    else if (item.ProjectItem != null)
    {
      //walking files
      ...
      else if (item.ProjectItem.ProjectItems != null)
      if (item.ProjectItem.ProjectItems.Count > 0)
        WalkProjectItemTree(item.ProjectItem);
    }
  }
  
private void WalkProjectItemTree(object CurrentItem)
{
  Project CurProject = null;
  CurProject = CurrentItem as Project;
  if (CurProject != null)
  {
    IVsHierarchy projectHierarchy = ToHierarchy(CurProject);
    PatternsForActiveConfigurations.Clear();
    EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, 
      projectHierarchy, 0, false);

    return;
  }
    ProjectItem item = null;
    item = CurrentItem as ProjectItem;
    if (item != null)
    {
        ...
        if (item.ProjectItems != null)
            if (item.ProjectItems.Count > 0)
            {
                foreach (object NextItem in item.ProjectItems)
                    WalkProjectItemTree(NextItem);
            }
    }
}

Проектная модель Visual C++. Конфигурации и свойства проектов и файлов

Ранее мы рассматривали обобщённую, внешнюю часть проектной модели Visual Studio, интерфейсы которой определены в EnvDTE и доступны из любой реализации модели автоматизации. Остановимся теперь на одной из базовых реализаций проектной модели - Microsoft Visual C++, которая определена в пространстве имён Microsoft.VisualStudio.VCProjectEngine.

Проектная модель Visual C++ реализует стандартную проектную модель Visual Studio, поэтому интерфейсы, описанные в первом подразделе могут использоваться в том числе и для работы с проектами этой модели. Интерфейсы, специфичные для модели Visual C++, определены в файле Microsoft.VisualStudio.VCProjectEngine.dll, который необходимо добавить в список используемых сборок разрабатываемого проекта расширения.

Модель Visual C++ физически хранит сборочные параметры (параметры компиляции, линковки, пред и после-сборочные шаги, параметры запуска сторонних утилит и т.п.) С/С++ файлов исходного кода в своих проектных xml файлах (vcproj/vcxproj). Данные параметры доступны пользователю Visual Studio через интерфейс диалоговых окон страниц свойств (Property Pages).

Наборы свойств определены для каждого сочетания сборочной конфигурации проекта (например, Debug и Release) и сборочной платформы (Win32, x64, IA64 и т.п.). При этом такие наборы свойств определены на уровне всего проекта, а отдельные свойства могут быть переопределены для каждого конкретного файла (по умолчанию свойства файла наследуются от проекта). Какие именно свойства могут быть переопределены зависит от типа файла, например для заголовочных файлов доступно переопределение только свойства ExcludedFromBuild, тогда как для cpp файла возможно переопределение любого свойства компиляции.

Получение конфигураций

В проектной модели Visual C++ страницы свойств представлены через интерфейсы VCConfiguration (для проекта) и VCFileConfiguration (для файла). Для получения данных объектов будем отталкиваться от объекта ProjectItem, представляющего собой абстрактный элемент Solution дерева среды.

ProjectItem item;
VCFile vcfile = item.Object as VCFile;
Project project = item.ContainingProject;
String pattern = "Release|x64";
if (String.IsNullOrEmpty(pattern))
  return null;

VCFileConfiguration fileconfig = null;
IVCCollection fileCfgs = (IVCCollection)vcfile.FileConfigurations;
fileconfig = fileCfgs.Item(pattern) as VCFileConfiguration;
if (fileconfig == null)
  if (fileCfgs.Count == 1)
    fileconfig = (VCFileConfiguration)fileCfgs.Item(0);
        

В данном примере мы получили файловую конфигурацию для объекта VCFile (С/C++ заголовочный или исходный файл) с помощью метода Item(), передав в него pattern (имя конфигурации и имя платформы) нужной нам конфигурации. Pattern сборочной конфигурации определён на уровне проекта. Приведём пример получения активной (выбранной в интерфейсе IDE) конфигурации проекта.

ConfigurationManager cm = project.ConfigurationManager;
Configuration conf = cm.ActiveConfiguration;
String platformName = conf.PlatformName;
String configName = conf.ConfigurationName;
String pattern = configName + "|" + platformName;
return pattern;

Свойство ActiveConfiguration следует использовать с осторожностью, т.к. достаточно часто мы сталкивались с исключениями при частом обращении к нему из IDE модуля-расширения PVS-Studio. В частности, данное поле оказывается недоступным через объектную модель автоматизации в моменты, когда пользователь Visual Studio осуществляет действия по сборке проектов либо просто взаимодействует с интерфейсом среды. Так как невозможно однозначно предсказать подобные действия со стороны пользователя, рекомендуется дополнительно обрабатывать подобные ситуации в механизмах доступа к свойствам объектов модели автоматизации. Заметим, что данная конкретная ситуация не является подвидом COM исключений, обработка которых рассматривалась в разделе, посвящённом EnvDTE интерфейсам, и скорее всего связана с недоработками в самой модели автоматизации.

Получим теперь конфигурацию проекта, содержащего данный файл:

VCConfiguration cfg=(VCConfiguration)fileconfig.ProjectConfiguration;

Непосредственно сами интерфейсы конфигураций содержат только общие свойства вкладки General, а свойства каждого конкретного сборочного инструмента определены в объектах, ссылки на которые доступны через поле VCConfiguration.Tools или VCFileConfiguration.Tool (одному файлу соответствует только один сборочный инструмент).

Рассмотрим, например, интерфейс, описывающий параметры C++ компилятора VCCLCompilerTool:

ct = ((IVCCollection)cfg.Tools).Item("VCCLCompilerTool") as 
  VCCLCompilerTool;
ctf = fileconfig.Tool as VCCLCompilerTool;

Получим для примера содержимое поля AdditionalOptions настроек компилятора, используя для вычисления значений макросов в этом поле метод Evaluate.

String ct_add = fileconfig.Evaluate(ct.AdditionalOptions);
String ctf_add = fileconfig.Evaluate(ctf.AdditionalOptions);

Property Sheets

Файлы свойств (property sheets) представляют собой XML файлы с расширением props, позволяющие независимо определять сборочные свойства проекта (т.е. параметры запуска различных сборочных инструментов, таких, как компилятор или линковщик). Property sheets поддерживают наследование и могут быть использованы для определения сборочных конфигураций в нескольких проектах одновременно, т.е. конфигурация, определённая в файле проекта (vcproj/vcxproj), может наследовать часть своих свойств из одного или нескольких props файлов.

Для работы с файлами свойств (Property sheets) проектная модель Visual C++ предоставляет интерфейс VCPropertySheet. Получить доступ к совокупности объектов VCPropertySheet проекта можно через поле VCConfiguration. PropertySheets:

IVCCollection PSheets_all = fileconfig.PropertySheets;

Аналогично поле PropertySheets интерфейса VCPropertySheet позволит получить ссылку на все дочерние файлы настроек для заданного объекта. Рассмотрим пример рекурсивного обхода всех файлов настроек проекта:

private void ProcessAllPropertySheets(VCConfiguration cfg, 
  IVCCollection PSheets)
{
  foreach (VCPropertySheet propertySheet in PSheets)
  {
    VCCLCompilerTool ctPS = 
      (VCCLCompilerTool)((IVCCollection)propertySheet.Tools).Item(
      "VCCLCompilerTool");

  if (ctPS != null)
  {
    ...
            
    IVCCollection InherPSS = propertySheet.PropertySheets;
    if (InherPSS != null)
      if (InherPSS.Count != 0)
        ProcessAllPropertySheets(cfg, InherPSS);
      }
    }
}

В приведённом примере мы получаем объект типа VCCLCompilerTool (свойства компилятора) для PropertySheet каждого уровня. Таким образом, мы сможем собрать все параметры компиляции, определённые во всех файлах свойств проекта, в том числе и во вложенных.

Интерфейс VCPropertySheet не содержит методов для вычисления макросов в своих полях, поэтому для этого приходится использовать тот же метод Evaluate конфигурации проекта. Такая практика, однако, может приводить к неправильному поведению в случае, если значение вычисляемого макроса связано непосредственно с props файлом. Например, ряд макросов MSBuild, появившихся в 4 версии, могут также быть использованы внутри новых проектов vcxproj из Visual Studio 2010. Макрос MSBuildThisFileDirectory, к примеру, раскрывается в путь до папки текущего файла, и поэтому вычисление его через cfg.Evaluate раскроет его до пути к vcxproj файла, а не к props файлу, в котором он используется.

Все страницы свойств проекта Visual C++ можно разделить на пользовательские и системные файлы. При этом под пользовательскими мы подразумеваем props файлы, непосредственно созданные и включённые в проект самим пользователем. Однако как можно заметить, даже пустой MSVC проект очень часто включает несколько props страниц по умолчанию. Такие системные props файлы используются средой для фактического определения ряда компиляционных параметров, задаваемых в странице настроек самого проекта. Например, задание параметра CharacterSet через юникод приведёт к появлению в списке Property Sheets данного проекта системного props файла, определяющего несколько символов препроцессора (Unicode, _Unicode). Поэтому, при обработке свойств, хранящихся в props файлах, следует помнить, что заданные в системных файлах символы компиляции также определены и через соответствующее им свойство в конфигурации уже самого проекта, доступной через API модели автоматизации. Очевидно, что получение настроек через эти два механизма приведёт к дублированию таких аргументов.

Проектная модель Atmel Studio. Настройки компиляции в сборочном инструментарии проекта

Ранее мы рассмотрели реализацию проектной модели Visual Studio для C/C++ проектов из Microsoft Visual C++, по умолчанию включённую в дистрибутиве Visual Studio. Однако модель автоматизации Visual Studio расширяема, и может быть дополнена интерфейсами для взаимодействия с проектными моделями сторонних разработчиков (такая сторонняя модель может быть реализована в модуле-расширении VSPackage). Поэтому, если разработчик сторонней проектной модели предоставляет для неё интерфейсы, мы сможем взаимодействовать с ней так же, как мы взаимодействовали и со стандартными моделями, например с моделью Visual C++, как было описано выше.

Для примера рассмотрим интерфейсы проектной модели, предоставляемой средой для разработки embedded решений Atmel Studio. Возможно, вы спросите – при чём здесь Atmel Studio, когда мы рассматриваем работу со средой Visual Studio? Однако сама Atmel Studio является изолированной оболочкой Visual Studio (Isolated Shell). Сейчас мы не будем останавливаться подробно на том, что из себя представляют изолированные оболочки среды, скажем лишь, что для них также возможна разработка всех тех же стандартных видов плагинов и расширений Visual Studio, что и для обычных версий данной среды. Вы можете более подробно ознакомиться с особенностями разработки плагинов под Visual Studio, включая и isolated shell редакции, в предыдущих разделах данного цикла.

Проектная модель Atmel является реализацией стандартной проектной модели Visual Studio. Так же, как и для проектов Visual C++, общие интерфейсы, могут применяться с проектами Atmel Studio. Интерфейсы, специфичные для данной модели, определены в файлах AvrGCC.dll, AvrProjectManagement.dll и Atmel.Studio.Toolchain.Interfaces.dll, которые можно получить, скачав специальный пакет Atmel Studio Extension Developer's Kit (XDK).

Физически проектная модель Atmel Studio хранит сборочные параметры в файлах проектов cproj, которые, являются проектными файлами сборочной платформы MSBuild (как, впрочем, и все стандартные типы проектов в Visual Studio). Проекты Atmel Studio поддерживают языки программирования C/C++ и используют для сборки специальную версию компиляторов GCC.

Типы проектов и сборочных инструментариев

Atmel Studio предоставляет 2 типа проектов: C и C++ проекты. Обратите внимание, что данные проекты имеют разный GUID идентификатор, что нужно учитывать при обходе проектного дерева.

Проектная модель Atmel также предоставляет и 2 набора сборочных инструментариев – GNU C compiler и GNU C++ Compiler, каждый со своими обособленными настройками. Стоит однако заметить, что тогда как C проекты могут содержать только настройки для C компилятора, C++ проекты содержат в своём инструментарии как C так и C++ настройки. А при компиляции будет выбран тот набор настроек, который соответствует собираемому файлу, т.е. в случае "смешанного" проекта настройки будут браться из 2-х наборов!

Проверить доступные наборы настроек для каждого конкретного проекта можно с помощью интерфейса ProjectToolchainOptions.

ProjectItem item;
...
AvrGccFileNode file = item.Object as AvrGccFileNode;
AvrGCCNode project = file.ProjectMgr as AvrGCCNode;
AvrProjectConfigProperties ActiveProps =
  project.ConfigurationManager.GetActiveConfigProperties();
ProjectToolchainOptions ToolChainOptions =
  ActiveProps.ToolchainOptions;
if (ToolChainOptions.CppCompiler != null)
    //Toolchain compiler options for C++ compiler
if (ToolChainOptions.CCompiler != null)
    //Toolchain compiler options for C Compiler

Получение настроек компиляции

Для получения непосредственно самих настроек компиляции можно воспользоваться полученным нами ранее объектом типа CompilerOptions (базовый тип для CppCompilerOptions и CCompilerOptions). Ряд настроек можно напрямую взять из объекта, например Include пути проекта:

CompilerOptions options;
...
List<String> Includes = options. IncludePaths;

Заметьте, что часть настроек являются общими для все наборов (т.е. для C и C++ настроек). Например, это общие системные Include пути:

List<String> SystemIncludes = options. DefaultIncludePaths;

Но большая часть настроек доступна через Dictionary<String, IList<String>> свойство OtherProperties. Как видно, каждому свойству (ключу) может соответствовать список из одного или более значений.

Если же вам требуется полная строка, передаваемая из проекта в MSBuild (и, соответственно, в компилятор), а не отдельные настройки сборки, то такую строку можно сразу получить с помощью свойства CommandLine (значительно проще, чем в VCProjectEngine!):

String RawCommandLine = this.compilerOptions.CommandLine;

Однако полученная подобным способом командная строка всё равно не будет содержать General настроек, таких, как системные Include пути.

Стоит, однако, помнить, что как отдельные настройки, полученные данным способом, так и полная командная строка, могут содержать нераскрытые MSBuild макросы. Для их раскрытия воспользуемся методом GetAllProjectProperties интерфейса AvrGCCNode:

AvrGCCNode project;
...
Dictionary<string, string> MSBuildProps = new Dictionary<string,
  string>();
project.GetAllProjectProperties().ForEach(x =>MSBuildProps.Add(x.Key,
  x.Value));

Теперь мы сможем заменить все встречающиеся в полученных настройках макросы соответствующими значениями из коллекции MSBuildProps.

Для каждого файла в проекте могут быть заданы дополнительные сборочные параметры, а точнее – дополнительные флаги, передаваемые компилятору только для этого файла. Получим эти дополнительные параметры с помощью интерфейса AvrFileNodeProperties:

AvrGccFileNode file;
...
AvrFileNodeProperties FileProps =  file.NodeProperties as
  AvrFileNodeProperties;
String AdditionalFlags = FileProps.CustomCompilationSetting;

Рекомендуемые ссылки



Используйте PVS-Studio для поиска ошибок в C, C++ и C# коде

Предлагаем попробовать проверить код вашего проекта с помощью анализатора кода PVS-Studio. Одна найденная в нём ошибка скажет вам о пользе методологии статического анализа кода больше, чем десяток статей.

goto PVS-Studio;


А ты совершаешь ошибки в коде?

Проверь с помощью
PVS-Studio

Статический анализ
кода для C, C++ и C#

goto PVS-Studio;