Руководство по разработке модулей расширений на C# для Visual Studio 2005-2012 и Atmel StudioИсточник: habrahabr
АннотацияОколо года назад мы опубликовали в блоге цикл статей о разработке плагинов для Visual Studio на языке C#. Сейчас мы переработали эти материалы, добавили новые разделы и предлагаем вашему вниманию новый вариант руководства. Мы занимаемся разработкой статического анализатора кода PVS-Studio. И хотя сам инструмент предназначен для программистов на C++, немалая его часть написана на C#. Когда мы начинали разрабатывать наш плагин, в мире Visual Studio самой новой и современной считалась версия Visual Studio 2005. И хотя сейчас, когда уже вышла Visual Studio 2012, некоторые могут сказать, что Visual Studio 2005 не совсем актуальна, мы до сих пор поддерживаем эту версию в своем инструменте. За то время что мы поддерживали разные версии Visual Studio и использовали разные возможности среды, у нас накопилось большое количество практических знаний о том, как же правильно (а особенно неправильно!) создавать плагины. Держать в себе эти знания больше не было никаких сил. Поэтому мы решили оформить их и опубликовать. Ведь некоторые решения, которые сейчас кажутся очевидными, были найдены только несколько лет спустя. А те, проблемы, которые уже давно решены, до сих пор могут мучать некоторых разработчиков плагинов. Будут рассмотрены следующие вопросы:
Более детальное и полное описание затронутых в статье тем доступно по приведённым в конце каждого раздела ссылкам на официальные материалы библиотеки 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, cоздание пакета расширенияРассмотрим создание подключаемого модуля вида 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. Вообще очень важно понимать, как стартует и как завершается модуль расширения. Ведь может оказаться, что разработчик пытается использовать какой-то функционал 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). Его можно запустить, добавив в строку аргументов запуска среды специальный параметр: Visual Studio SDK предоставляет специальную утилиту для создания или очистки экспериментальных экземпляров - CreateExpInstance. Вызов CreateExpInstance для создания новой экспериментальной ветки выглядит следующим образом: После установки 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. После его обнаружения будет произведён поиск дополнительных регистрационных атрибутов. Атрибут ProvideMenuResource задаёт ID ресурсов пользовательских команд и меню для их регистрации в IDE. При необходимости добавления в системный реестр во время регистрации пакета расширения пользовательских ключей или необходимости добавления значений для уже существующих ключей, возможно создание пользовательских регистрационных атрибутов путём наследования от абстрактного класса RegistrationAttribute. Для добавления регистрационной информации в реестр можно воспользоваться утилитой 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 файла (в одну строку): В PVS-Studio мы обходимся без запуска RegPkg, а вручную добавляем нужную информацию в реестр во время установки. Это делается из тех соображений, чтобы не зависеть от еще одной сторонней утилиты, а полностью самим контролировать процесс установки. Но мы всё же используем RegPkg при разработке плагина для удобства его отладки.
VSIX пакетыНачиная с версии Visual Studio 2010, появилась возможность существенно упростить развёртывание VSPackage модулей с помощью VSIX пакетов. VSIX пакет представляет из себя стандартный OPC (Open Packaging Conventions) архив, содержащий бинарные файлы плагина и все необходимые для их развёртывания вспомогательные файлы. Данный архив может быть передан стандартной утилите VSIXInstaller.exe, которая автоматически зарегистрирует содержащиеся в нём модули-расширения.
Для включения в контейнер дополнительных файлов из проекта необходимо задать для этих файлов в MSBuild проекте узел IncludeInVSIX (можно также отметить такие файлы в SolutionExplorer через окно Properties). 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. И еще раз стоит напомнить о необходимости тестировать готовый дистрибутив на машине без установленного 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:
Рекомендуемые ссылки
Объектная модель автоматизации 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 (нажмите на рисунок для увеличения) Модель может быть расширена пользователем в следующих функциональных группах:
Расширение модели автоматизации доступно только для модулей 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, что может произойти как непосредственно в момент загрузки среды, так и после первого обращения к нему. Получение ссылки выглядит следующим образом: Если Add-In загружается при старте Visual Studio (ext_ConnectMode.ext_cm_Startup), то в момент получения управления методом OnConnect среда может ещё быть не полностью проинициализирована. В таком случае инициализацию непосредственно DTE интерфейса предпочтительно отложить до окончания инициализации самой среды. Для этого можно воспользоваться предоставляемым интерфейсом IDTExtensibility обработчиком OnStartupComplete. VSPackage расширениеДля расширения типа VSPackage интерфейс DTE может быть получен как глобальный сервис Visual Studio с помощью метода GetService класса Package:
Независимый внешний процессDTE является верхне-уровневой абстракцией для самой среды Visual Studio в модели автоматизации. Для получения ссылки на него из внешнего приложения нужно использовать его ProgID COM идентификатор, например VisualStudio.DTE.10.0 для Visual Studio 2010. Приведём пример инициализации нового экземпляра IDE и получения ссылки на её DTE интерфейс.
Документы текстового редактора Visual StudioМодель автоматизации определяет текстовые документы Visual Studio через интерфейс текстового документаTextDocument. Исходные C/C++ файлы открываются средой как текстовые документы. TextDocument основан на общем интерфейсе документов модели автоматизации (интерфейс Document), описывающем любой открытый в редакторе или дизайнере Visual Studio файл. Ссылку на объект текстового документа можно получить через поле Object объекта Document. Получим, например, текстовый документ для активного (т.е. открытого и имеющего фокус) документа из текстового редактора IDE. Редактирование документовИнтерфейс TextSelection позволяет модифицировать текст и управлять выделениями. Методы данного интерфейса отражают функционал текстового редактора Visual Studio, т.е. позволяют работать непосредственно с видимым в UI текстом. Также интерфейс TextDocument позволяет редактировать текст документа с помощью интерфейса EditPoint. Данный интерфейс схож с интерфейсом TextSelection, но в отличие от него позволяет манипулировать данными текстового буфера, а не текстом, отображённым непосредственно в редакторе. Отличие в том, что на текстовый буфер не влияют такие параметры редактора, как WordWrap и Virtual Spaces. Заметим, что для данных методов редактирования не доступны участки текста из read-only блоков. Приведём пример редактирования с помощью EditPoint, в котором текст добавляется в конце текущей строки редактирования (строки с курсором). Навигация по документамДля модулей типа VSPackage доступен ряд глобальных служб среды, позволяющих открывать и просматривать документы. Данные службы можно получить с помощью Managed Package Framework метода Package.GetGlobalService(). Заметим, что используемые здесь интерфейсы не являются частью модели EnvDTE, доступны только для объекта пакета расширения Package и поэтому не могут быть использованы в других видах расширений Visual Studio. Тем не менее, эти службы удобны для использования при работе с IDE документами совместно с описанным выше интерфейсом Document, поэтому мы остановимся на них более подробно в данном разделе. Интерфейс IVsUIShellOpenDocument позволяет контролировать состояние открытых документов среды. Приведём пример открытия документа через путь до связанного с ним файла. Подписка и обработка событийСобытия объектов автоматизации определены в корневом элементе DTE.Events. При этом данный интерфейс содержит ссылки как на стандартные (общие) события IDE (CommandEvents, SolutionEvents), так и на события различных элементов среды (отдельные типы проектов, редакторы, инструменты и т.п.), в том числе и определённых пользователем. Для получения ссылки для подобного объекта автоматизации можно использовать метод GetObject. При подписке на события DTE стоит помнить, что в момент инициализации расширения данный интерфейс может быть ещё не доступен. Поэтому важно помнить и учитывать порядок инициализации вашего разрабатываемого модуля в случае, если подписка на DTE.Events потребуется именно в момент инициализации плагина-расширения. При этом правильная обработка этого момента для получения ссылки на DTE будет выглядеть по-разному для разных версий расширений, как было описано в предыдущем разделе. Получим ссылку для событий проектной модели Visual C++, определённой интерфейсом VCProjectEngineEvents и назначим обработчик на удаление элемента из Solution дерева: События MDI оконДля обработки стандартных событий MDI окна среды можно воспользоваться интерфейсом Events.WindowEvents. Данный интерфейс позволяет назначить как отдельный обработчик для окна (определённого через интерфейс EnvDTE.Window), так и общий обработчик для всех окон среды. Назначение общего обработчика для среды на переключение между окнами: События IDE командНепосредственно работа с командами и их расширение через модель автоматизации рассматриваются в отдельном разделе. Здесь мы затронем вопрос обработки событий команд (но не самого выполнения команд). Назначение обработчиков на события возможно с помощью интерфейса Events.CommandEvents. Свойство CommandEvents, по аналогии с обработкой событий MDI окон, также позволяет назначить обработчик как для всех команд 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 и т.д. Например: Интерфейс IVsSolutionLoadEvents, аналогично рассмотренному выше интерфейсу, должен быть реализован подклассом Package и доступен для версий среды начиная с Visual Studio 2010. Этот интерфейс позволит отслеживать такие интересные моменты, как погрузка группы проектов в пакетном режиме и отложенная (фоновая) подгруздка решения (методы OnBeforeLoadProjectBatch и OnBeforeBackgroundSolutionLoadBegins), а также отловить завершение фоновой погрузки (метод OnAfterBackgroundSolutionLoadComplete). Эти обработчики могут оказаться незаменимыми в случае, если вашему плагину необходимо выполнить какой-нибудь код сразу после своей инициализации, и при этом он имеет зависимости как раз от загруженных проектов\решений. Выполнение подобного кода без ожидания полной загрузки решения может в таком случае привести как к некорректным (неполным) результатом в связи с тем, что дерево проектов построено не полностью, так и к runtime исключениям. При разработке Visual Studio плагина PVS-Studio мы столкнулись с ещё одним интересным аспектом инициализации VSPackage модулей. Так если один Package модуль, не завершив своей инициализации, находится в состоянии ожидания (например, показывая пользователю диалог), то инициализация других модулей-расширений блокируется. Поэтому, обрабатывая события загрузки и инициализации в среде, следует помнить и о таком сценарии. Ещё раз обращаем ваше внимание на то, что для корректной работы методов, описанных в данных интерфейсах, необходимо унаследовать от них ваш основной класс плагина: Поддержка цветовой схемы среды Visual StudioВ случае если разрабатываемый вами плагин интегрируется в интерфейс среды разработки, например добавлением в неё собственных инструментальных или документных MDI окон (а такую интеграцию удобнее всего реализовать на основе VSPackage модуля), цветовую схему ваших интерфейсных компонентов желательно привести в соответствие с общей цветовой схемой среды Visual Studio. Особенно актуальной эта задача стала с появление в Visual Studio 2012 двух сильно отличающихся цветовых схем (Dark и Light), которые пользователь может переключать в настройках среды "на лету". Для "подхватывания" цветовых настроек среды можно воспользоваться методом GetVSSysColorEx интерфейсаIVsUIShell2 из Visual Studio Interop. Данный интерфейс будет доступен только VSPackage модулям. Если же вы не имеете по какой-то причине доступа к объекту 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 перед непосредственным запуском потока: Реализация фильтра ошибок доступа к 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 приложении. Рекомендуемые ссылки
Команды 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:
При использовании параметра запуска command, имя команды вместе со всеми её аргументами следует обернуть в двойные кавычки: Поэтому для интеграции статического анализа 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 содержит задаваемые по умолчания сочетания для быстрого вызова команд. И затем указать на него с помощью атрибута ProvideMenuResource у вашего наследника класса Package: Работа с командами через интерфейс EnvDTE.DTEОбъект автоматизации EnvDTE.DTE также предоставляет возможности для прямой программной манипуляции (создание, модификация, исполнение) команд через интерфейс dte.Commands и метод dte.ExecuteCommand. Использование объектной модели автоматизации для вызова, модификации и создания IDE команд, в отличие от механизма VSCT, доступного только для VSPackage, позволяет взаимодействовать с командами из модулей-расширений Visual Studio типа Add-In. Объект автоматизации DTE позволяет напрямую создавать, модифицировать и вызывать команды через интерфейсDTE.Commands. Метод Commands.AddNamedCommand позволяет добавить команду в IDE (только для Add-In модуля): Интерфейс EnvDTE.Command абстрагирует в себе отдельную команду IDE. Его можно использовать для модификации связанной с ним команды. Данный интерфейс позволяет работать с командами среды как из VSPackage, так и из Add-In модулей. Получим ссылку на объект автоматизации EnvDTE.Command для нашей пользовательской команды MyCommand1 и используем данный интерфейс для назначения ей "горячей клавиши" быстрого вызова: Стоит помнить, что команда Visual Studio не является по умолчанию элементом интерфейса IDE. Метод интерфейса Commands.AddCommandBar позволяет создавать такие элементы UI среды, как пункты главного меню, инструментальные панели, контекстные меню, и назначать на них пользовательские команды. Любая команда среды (как пользовательская, так и встроенная) может быть вызвана с помощью с помощью метода ExecuteCommand. Приведём пример вызова пользовательской команды MyCommand1: Рекомендуемые ссылки
Инструментальные окна 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: Стоит заметить, что инициализация пользовательского окна в VSPackage (что будет описано чуть позднее) не обязательно совпадает с инициализацией самого подкласса Package, для которого мы и указываем данный регистрационный атрибут. Например, после реализации инструментального окна для плагина PVS-Studio мы сталкивались с ситуацией, при которой окно оказывалось открытым (но не активным) среди нижних вкладок окон среды сразу после запуска Visual Studio, даже несмотря на то, что в атрибут ProvideToolWindow было передано Transient = true. При этом, несмотря на то, что сам плагин проходил инициализацию всегда на старте IDE, само окно оказывалось не до конца инициализированным вплоть до первого к нему обращения, что в частности было видно по неподгруженной иконке на вкладке. Для окна может быть задана динамическая область видимости с помощью атрибута ProvideToolWindowVisibility, например: Для создания и отображения инструментального окна из VSPackage можно воспользоваться методомFindToolWindow класса Package. Данный метод возвращает ссылку на указанный toolwindow объект, создавая его в случае необходимости (при первом запуске single-instance окна). Пример вызова и отображения single-instance окна: Для создания Multi-Instance инструментальных окон можно использовать метод CreateToolWindow, позволяющий создать окно с заданным идентификатором. Тогда метод вызова toolwindow будет выглядеть следующим образом: При работе с single-instance окном, после первого вызова среда будет запоминать его последующие положение и размеры, сохраняя их каждый раз перед закрытием. Однако если по какой-либо причине всё же потребуется вручную задать размер и положение окна, это можно сделать с помощью метода SetFramePos класса IVsWindowFrame:
Создание и вызов окна в Add-InДля модуля типа Add-In инициализировать пользовательское инструментальное окно можно с помощью методов EndDTE интерфейса Window2:
Реализация пользовательского инструментального окна в модуле VSPackageИнструментальные окна состоят из каркаса-рамки клиентской области. При этом каркас окна предоставляется средой и отвечает за стыковку с другими компонентами интерфейса (docking), размер и положение окна. Клиентская область (pane) отображает содержимое окна, контролируемое пользователем. Инструментальные окна могут содержать пользовательские WinForms и WPF компоненты и предоставляют возможность обрабатывать такие стандартные события, как например OnShow, OnMove и т.п. Пользовательское инструментальное окно, а точнее его клиентская область, реализуется путём наследования от класса, реализующего стандартное пустое окно IDE - ToolWindowPane: Атрибут Guid уникально идентифицирует каждое пользовательское окно. В случае если модуль создаёт несколько окон разного типа, каждое из них должно иметь свой уникальный Guid. Подкласс ToolWindowPane может в дальнейшем быть модифицирован для отображения в нём пользовательских компонентов и управления его состоянием.
Хостинг пользовательских компонентовБазовый класс ToolWindowPane реализует пустое инструментальное окно среды. Наследование от данного класса позволяет отобразить в этом окне пользовательские WinForms или WPF компоненты. До версии Visual Studio 2008 инструментальные окна нативно поддерживали отображение WinForms пользовательских компонентов, а также могли отображать WPF компоненты с помощью WPF Interoperability компонента ElementHost. Начиная с Visual Studio 2010, инструментальные окна стали основываться на технологии WPF, но всё еще поддерживают загрузку и отображение WinForms компонентов в режиме совместимости. Для отображения в окне пользовательского WinForms компонента можно воспользоваться переопределённым свойством Window у ToolWindowPane: Начиная с версии Visual Studio 2010 появилась возможность нативно отображать WPF компоненты. Для этого нужно передать ссылку на ваш WPF компонент полю Content базового класса: В основном окне 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. Приведём пример реализации данного интерфейса: Контроль состояния окнаКонтроль состояния окна можно осуществлять с помощью обработчиков событий нашей реализации интерфейса 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. Шестой bool аргумент конструктора атрибута позволяет зарегистрировать пользовательскую страницу как объект автоматизации. Это позволит получить доступ к настройкам, определённым в данной странице через интерфейсы EnvDTE, из сторонних plug-in модулей. Регистрация объекта автоматизации требует создания записей в системном реестре (что происходит автоматически при использовании данных атрибутов) в следующих ветках:
Реализация наследования от MPF класса DialogPageМинимальным требованием к подклассу DialogPage для реализации пользовательской страницы настроек является наличие в классе-наследнике открытых свойств (public properties). Подобная базовая реализация будет выглядеть следующим образом: Для отображения в окне настроек пользовательского интерфейса необходимо переопределить свойство Windowкласса DialogPage: Через свойство AutomationObject пользовательской страницы настроек, унаследованной от класса DialogPage, определяются те открытые свойства, которые будут отображены через стандартные компоненты в окне настроек и к которым будет применён стандартный IDE механизм сохранения состояния. По умолчанию AutomationObject возвращает ссылку на сам подкласс DialogPage, но если он вернёт ссылку на какой-либо другой объект, то открытые свойства именно этого объекта будут использованы для отображения и сохранения состояния настроек. В стандартной реализации системный реестр используется как локальное хранилище настроек. Переопределение метода подкласса DialogPage.SaveSettingsToStorage позволяет изменить способ работы механизма сохранения состояния настроек среды (по аналогии, переопределение метода LoadSettingsFromStorage позволяет изменить работу механизма восстановления настроек). Заметим, что интеграция в диалог настроек 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 модуля, содержащего определение для пользовательской страницы настроек. 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: Для открытия страницы пользовательской настроек в диалоге Options можно воспользоваться методом ShowOptionPage MPF класса Package: Рекомендуемые ссылки
Проектная модель 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++ также следует данной общей схеме проектной автоматизации: Рекурсивный обход всех элементов ветви Solution дереваДля обхода ветви Solution дерева можно воспользоваться интерфейсом управления иерархиями IVsHierarchy. Интерфейс предоставляет доступ к абстрактным узлам дерева, каждый из которых может являться листом, контейнером элементов или ссылкой на другую иерархию. Каждый узел дерева уникально идентифицируется через DWORD идентификатор VSITEMID. Такие идентификаторы уникальны в рамках одной иерархии и имеют в ней ограниченный срок существования. Объект иерархии можно получить для ветки отдельного проекта с помощью метода VsShellUtilities.GetHierarchy: Практика разработки IDE плагина PVS-Studio, однако, показывает, что возможна и обратная ситуация, т.е. пользовательский тип проекта, использующий GUID одного из стандартных проектных типов, причём обычно тот, от которого он наследовался. В частности, мы столкнулись с типом проектов VCProject, расширенным для разработки под платформу Android. Как результат, данное расширение проектной модели приводило к падению нашего модуля, т.к. оно не предоставляло через API интерфейсы некоторые из присутствующих в VCProject полей (например, поддержка OpenMP). Сложность данной ситуации в том, что такой расширенный проектный тип невозможно дифференцировать от обычных проектов, и соответственно, корректно обработать его. Поэтому при расширении проектной модели через пользовательские типы, для избегания конфликтов с другими (в том числе и пользовательскими) компонентами IDE, стоит помнить о необходимости обеспечения возможности для её уникальной идентификации. Имея объект иерархии IVsHierarchy проекта, мы можем осуществить рекурсивный обход всех элементов данной ветки Solution дерева с помощью метода hierarchy.GetProperty, позволяющего получить заданные свойства каждого узла иерархии:
Обход всех проектов Solution дереваДля обхода всех проектов дерева можно воспользоваться интерфейсом DTE.Solution.Projects: Обход выделенных элементовДля обхода элементов, выделенных пользователем в интерфейсе окна Solution Explorer, можно воспользоваться интерфейсом DTE.SelectedItems. Проектная модель 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 дерева среды. Получим теперь конфигурацию проекта, содержащего данный файл: Рассмотрим, например, интерфейс, описывающий параметры C++ компилятора VCCLCompilerTool: Property SheetsФайлы свойств (property sheets) представляют собой XML файлы с расширением props, позволяющие независимо определять сборочные свойства проекта (т.е. параметры запуска различных сборочных инструментов, таких, как компилятор или линковщик). Property sheets поддерживают наследование и могут быть использованы для определения сборочных конфигураций в нескольких проектах одновременно, т.е. конфигурация, определённая в файле проекта (vcproj/vcxproj), может наследовать часть своих свойств из одного или нескольких props файлов. Для работы с файлами свойств (Property sheets) проектная модель Visual C++ предоставляет интерфейсVCPropertySheet. Получить доступ к совокупности объектов VCPropertySheet проекта можно через поле VCConfiguration. PropertySheets: Интерфейс 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. Получение настроек компиляцииДля получения непосредственно самих настроек компиляции можно воспользоваться полученным нами ранее объектом типа CompilerOptions (базовый тип для CppCompilerOptions и CCompilerOptions). Ряд настроек можно напрямую взять из объекта, например Include пути проекта: Если же вам требуется полная строка, передаваемая из проекта в MSBuild (и, соответственно, в компилятор), а не отдельные настройки сборки, то такую строку можно сразу получить с помощью свойства CommandLine (значительно проще, чем в VCProjectEngine!): Стоит, однако, помнить, что как отдельные настройки, полученные данным способом, так и полная командная строка, могут содержать нераскрытые MSBuild макросы. Для их раскрытия воспользуемся методом GetAllProjectProperties интерфейса AvrGCCNode: Для каждого файла в проекте могут быть заданы дополнительные сборочные параметры, а точнее - дополнительные флаги, передаваемые компилятору только для этого файла. Получим эти дополнительные параметры с помощью интерфейса AvrFileNodeProperties: Рекомендуемые ссылки
|