Около года назад мы опубликовали в нашем блоге серию статей о разработке плагинов для Visual Studio на C #. Мы недавно пересмотрели эти материалы и добавили новые разделы, и теперь приглашаем вас взглянуть на обновленную версию руководства в виде серии статей здесь, на DZone.
Другие статьи в серии можно найти здесь:
- Часть 1. Как создавать, отлаживать и развертывать пакеты расширений Visual Studio
- Часть 2. Использование объектной модели автоматизации Visual Studio
- Часть 3 — Использование команд Visual Studio
- Часть 4. Интеграция пользовательских инструментов Windows в среду Visual Studio
- Часть 5. Расширение диалога настроек Visual Studio
- Часть 6 — Структура модели проекта Visual Studio
Эта статья (вторая в серии) содержит обзор объектной модели автоматизации Visual Studio. В нем мы рассмотрим общую структуру модели и способы получения доступа к ее интерфейсам через объекты верхнего уровня DTE / DTE2. Приведено несколько примеров использования элементов модели. Также обсуждаются вопросы использования интерфейсов модели в многопоточных приложениях; Также приведен пример реализации такого механизма многопоточного взаимодействия с интерфейсами COM в управляемом коде.
Вступление
Среда разработки Visual Studio построена на принципах автоматизации и расширяемости, предоставляя разработчикам возможность интегрировать практически любой пользовательский элемент в IDE и позволяя легко взаимодействовать с его компонентами по умолчанию и пользовательскими компонентами. В качестве средства реализации этих задач пользователям Visual Studio предоставляется несколько взаимодополняющих наборов инструментов; Наиболее простой и универсальной среди них является объектная модель автоматизации Visual Studio.
Объектная модель автоматизации представлена рядом библиотек, содержащих обширный и хорошо структурированный набор API, который охватывает все аспекты автоматизации IDE и большинство ее возможностей расширяемости. Хотя по сравнению с другими инструментами расширения IDE эта модель не обеспечивает доступа к некоторым частям Visual Studio (это относится главным образом к расширению некоторых функций IDE), тем не менее она является наиболее гибкой и универсальной среди них.
Большинство интерфейсов модели доступны из каждого типа модуля расширения IDE, что позволяет взаимодействовать со средой даже из внешнего независимого процесса. Более того, сама модель может быть расширена вместе с расширением Visual Studio IDE, предоставляя сторонним разработчикам доступ к пользовательским компонентам, созданным пользователем.
Структура объектной модели автоматизации
Модель автоматизации Visual Studio состоит из нескольких взаимосвязанных функциональных групп объектов, охватывающих все аспекты среды разработки; он также предоставляет возможности для контроля и расширения этих групп. Доступ к любому из них возможен через глобальный интерфейс DTE верхнего уровня (Среда средств разработки). На рисунке 1 показана общая структура модели автоматизации и ее распределение по функциональным группам.
Рисунок 1 — Объектная модель автоматизации Visual Studio
Пользователь может расширить модель в одну из следующих групп:
- Модели проектов (внедрение новых типов проектов, поддержка новых языков);
- Модели документов (внедрение новых типов документов и редакторов документов)
- Модели уровня редактора кода (поддержка определенных языковых конструкций)
- Модели уровня сборки проекта
Модель автоматизации может быть расширена только из плагинов типа VSPackage.
Все интерфейсы модели автоматизации можно условно разделить на две большие группы. Первая группа состоит из интерфейсов пространств имен EnvDTE и Visual Studio Interop. Эти интерфейсы позволяют взаимодействовать с основными общими компонентами самой IDE, такими как окна инструментов, редакторы, службы обработки событий и так далее. Вторая группа состоит из интерфейсов конкретной модели проекта. Рисунок выше определяет эту группу интерфейсов как свойства с поздним связыванием, т.е. эти интерфейсы реализованы в отдельной динамически загружаемой библиотеке. Каждая стандартная (т.е. та, которая включена в обычный дистрибутив Visual Studio) модель проекта, такая как Visual C ++ или Visual Basic, предоставляет отдельную реализацию для этих интерфейсов.Сторонние разработчики могут расширять модель автоматизации, добавляя свои собственные модели проектов и обеспечивая реализацию этих интерфейсов автоматизации.
Также стоит отметить, что интерфейсы 1-й группы, которые были указаны выше, являются универсальными, что означает, что они могут использоваться для взаимодействия с любой из моделей проекта или редакций Visual Studio, включая встроенные \ изолированные оболочки Visual Studio. В этой статье мы рассмотрим эту группу более подробно.
Но, тем не менее, несмотря на универсальность модели, не каждая группа, принадлежащая этой модели, может быть одинаково использована из всех типов расширений IDE. Например, некоторые возможности модели недоступны для внешних процессов; Эти возможности связаны с определенными типами расширений, такими как надстройка или VSPackage. Поэтому при выборе типа разрабатываемого расширения важно учитывать функциональность, которая потребуется для этого расширения.
Пространство имен Microsoft.VisualStudio.Shell.Interop также предоставляет группу интерфейсов COM, которые можно использовать для расширения и автоматизации приложения Visual Studio из управляемого кода. Классы Managed Package Framework (MPF), которые мы использовали ранее для создания плагина VSPackage, фактически сами основаны на этих интерфейсах. Хотя эти интерфейсы не являются частью модели автоматизации EnvDTE, описанной выше, тем не менее они значительно расширяют эту модель, предоставляя дополнительные функциональные возможности для расширений VSPackage, которые в противном случае недоступны для расширений других типов.
Получение ссылок на объекты DTE / DTE2
Чтобы создать приложение автоматизации Visual Studio, необходимо в первую очередь получить доступ к самим объектам автоматизации. Для этого прежде всего необходимо подключить правильные версии библиотек, содержащих необходимые управляемые оболочки API, в пространстве имен EnvDTE. Во-вторых, должна быть получена ссылка на объект верхнего уровня модели автоматизации, то есть интерфейс DTE2.
В ходе эволюции Visual Studio некоторые из ее объектов автоматизации были изменены или получили некоторые дополнительные функции. Поэтому для обеспечения обратной совместимости с существующими пакетами расширений вместо обновления интерфейсов из исходного пространства имен EnvDTE были созданы новые пространства имен EnvDTE80, EnvDTE90, EnvDTE100 и т. Д. Большинство таких обновленных интерфейсов из этих новых пространств имен поддерживают те же имена, что и в исходных, но с добавлением порядкового номера в конце имени, например Solution и Solution2. Рекомендуется использовать эти обновленные интерфейсы при создании нового проекта, так как они содержат самые последние функциональные возможности. Стоит отметить, что свойства и методы интерфейса DTE2 обычно возвращают ссылки на объекты с типами, соответствующими исходному DTE.Например, доступ к dte2.Solution вернет Solution, а не Solution2, как может показаться.
Хотя эти новые пространства имен EnvDTE80, EnvDTE90, EnvDTE100 содержат некоторые обновленные функции, как упомянуто выше, тем не менее, именно интерфейс EnvDTE содержит большинство объектов автоматизации. Поэтому, чтобы иметь доступ ко всем существующим интерфейсам, необходимо связать все версии управляемых библиотек оболочки COM с проектом, а также получить ссылки на DTE, а также на DTE2.
Способ получения ссылки на объект верхнего уровня EnvDTE зависит от типа разрабатываемого расширения IDE. Давайте рассмотрим три таких типа расширений: надстройка, VSPackage и независимый от MSVS внешний процесс.
Расширение надстройки
В случае расширения надстройки доступ к интерфейсу DTE можно получить с помощью метода OnConnection, который должен быть реализован для интерфейса IDTExtensibility, который обеспечивает доступ к событиям взаимодействия расширения с окружением. Метод OnConnection вызывается в тот момент, когда модуль загружается в IDE. Это может произойти либо во время загрузки среды, либо после первого вызова расширения в сеансе IDE. Пример получения ссылки следующий:
public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom) { _dte2 = (DTE2)application; ... }
Модуль расширения может быть инициализирован либо в момент запуска IDE, либо при его первом вызове в текущем сеансе IDE. Таким образом, connectMode можно использовать для правильного определения момента инициализации внутри метода OnConnection.
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; }
Как и в приведенном выше примере, надстройка может быть загружена либо одновременно с самой IDE (если отмечена опция запуска в диспетчере надстроек), когда она вызывается в первый раз или когда она вызывается из командной строки. Параметр ext_ConnectMode.ext_cm_UISetup вызывается только один раз в общем времени жизни плагина, то есть во время его первой инициализации. Этот случай следует использовать для инициализации элементов пользовательского интерфейса, которые должны быть интегрированы в среду (подробнее об этом позже).
Если надстройка загружается во время запуска Visual Studio (ext_ConnectMode.ext_cm_Startup), то в тот момент, когда метод OnConnect впервые получает управление, возможно, что IDE все еще не полностью инициализирована сама. В таком случае рекомендуется отложить получение ссылки на DTE до полной загрузки среды. Для этого можно использовать обработчик OnStartupComplete, предоставляемый IDTExtensibility.
public void OnStartupComplete(ref Array custom) { ... }
Расширение VSPackage
Для расширения типа VSPackage DTE можно получить через глобальную службу Visual Studio с помощью метода GetService подкласса Package:
DTE dte = MyPackage.GetService(typeof(DTE)) as DTE;
Обратите внимание, что метод GetService может потенциально вернуть ноль в случае, если Visual Studio не полностью загружен или инициализирован в момент такого доступа, то есть он находится в так называемом состоянии «зомби». Чтобы правильно справиться с этой ситуацией, рекомендуется отложить получение ссылки на 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; }
Следует отметить, что процесс инициализации модуля VSPackage при запуске IDE может отличаться для разных версий Visual Studio. Например, в случае VS2005 и VS2008 попытка доступа к DTE во время запуска IDE почти всегда приводит к возвращению нулевого значения, что связано с относительным быстрым временем загрузки этих версий. Но не просто получить доступ к DTE. В случае Visual Studio 2010 ошибочно представляется, что можно просто получить доступ к DTE из метода Initialize (). На самом деле, это впечатление ложное, поскольку такой метод получения DTE потенциально может вызвать случайное появление «плавающих» ошибок, которые трудно идентифицировать и отладить, и даже сам DTE может все еще быть неинициализирован при получении ссылки , Из-за этих различий,вышеупомянутый метод получения для обработки состояний загрузки IDE не следует игнорировать ни в одной версии Visual Studio.
Независимый внешний процесс
Интерфейс DTE — это абстракция верхнего уровня для среды Visual Studio в модели автоматизации. Чтобы получить ссылку на этот интерфейс из внешнего приложения, можно использовать его COM-идентификатор ProgID; например, это будет «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, запустив процесс deven.exe с помощью метода CreateInstance. Но в то же время окно графического интерфейса среды будет отображаться только после вызова метода Activate.
Далее рассмотрим простой пример получения ссылки на DTE из уже запущенного экземпляра Visual Studio:
EnvDTE80.DTE2 dte2; dte2 = (EnvDTE80.DTE2) System.Runtime.InteropServices.Marshal.GetActiveObject( "VisualStudio.DTE.10.0");
Однако если в момент нашего запроса выполняется несколько экземпляров Visual Studio, метод GetActiveObject вернет ссылку на экземпляр IDE, который был запущен раньше. Давайте рассмотрим возможный способ получения ссылки на 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. Ссылку на объект текстового документа можно получить через поле «Объект» объекта «Документ». Давайте получим текстовый документ для текущего активного (т.е. имеющего фокус) документа из текстового редактора IDE:
EnvDTE.TextDocument objTextDoc = (TextDocument)PVSStudio.DTE.ActiveDocument.Object("TextDocument");
Изменение документов
Документ TextSelection позволяет контролировать выделение текста или изменять его. Методы этого интерфейса представляют функциональные возможности текстового редактора Visual Studio, то есть позволяют взаимодействовать с текстом, как он представлен непосредственно в пользовательском интерфейсе.
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. Следует отметить, что оба эти метода редактирования не могут изменять текстовые блоки только для чтения.
Давайте рассмотрим пример изменения текста с помощью 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 могут получить доступ к ряду глобальных сервисов, которые можно использовать для открытия и обработки документов среды. Эти службы могут быть получены методом Package.GetGlobalService () из инфраструктуры управляемых пакетов. Следует отметить, что службы, описанные здесь, не являются частью модели EnvDTE и доступны только из расширения типа пакета, и поэтому их нельзя использовать в других типах расширений Visual Studio. Тем не менее, они могут быть весьма полезны для обработки документов IDE, когда они используются в дополнение к интерфейсу документов, описанному ранее. Далее мы рассмотрим эти услуги более подробно.
Интерфейс 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 в методе Initialize () вашего пакета расширения. Правильная обработка последовательности инициализации будет отличаться для разных типов расширений, как это было описано ранее.
Давайте заполним ссылку на объект событий модели проекта Visual C ++, определенный интерфейсом VCProjectEngineEvents, и назначим обработчик для удаления элемента из дерева Solution Explorer:
VCProjectEngineEvents m_ProjectItemsEvents = PVSStudio.DTE.Events.GetObject("VCProjectEngineEventsObject") as VCProjectEngineEvents; m_ProjectItemsEvents.ItemRemoved += new _dispVCProjectEngineEvents_ItemRemovedEventHandler( m_ProjectItemsEvents_ItemRemoved);
События MDI Windows
Свойство Events.WindowEvents может использоваться для обработки обычных событий окна MDI среды. Этот интерфейс позволяет назначать отдельный обработчик для одного окна (определенного через интерфейс EnvDTE.Window) или назначать общий обработчик для всех окон среды. Следующий пример содержит назначение обработчика для события переключения между окнами IDE:
WindowEvents WE = PVSStudio.DTE.Events.WindowEvents; WE.WindowActivated += new _dispWindowEvents_WindowActivatedEventHandler( Package.WE_WindowActivated);
Следующий пример — это назначение обработчика для переключения окна в текущее активное окно MDI через индексатор WindowEvents:
WindowEvents WE = m_dte.Events.WindowEvents[MyPackage.DTE.ActiveWindow]; WE.WindowActivated += new _dispWindowEvents_WindowActivatedEventHandler( MyPackage.WE_WindowActivated);
События команд IDE
Фактическая обработка команд среды и их расширение с помощью модели автоматизации рассматривается в отдельной статье этой серии. В этом разделе мы рассмотрим обработку событий, связанных с этими командами (а не выполнение самих команд). Назначение обработчиков этим событиям возможно через интерфейс Events.CommandEvents. Свойство CommandEvents, как и в случае событий окон MDI, также позволяет назначать обработчик либо для всех команд, либо для одной через индексатор.
Давайте рассмотрим назначение обработчика для случая завершения выполнения команды (т.е. когда команда заканчивает свое выполнение):
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
Теперь, после перезапуска IDE, при наведении курсора мыши на элементы меню или панели инструментов с одновременным нажатием 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 описана в отдельной статье, полностью посвященной командам IDE.
В заключение этого раздела следует отметить, что в процессе разработки нашего собственного расширения VSPackage мы столкнулись с необходимостью хранить ссылки на объекты интерфейса, содержащие делегаты наших обработчиков (такие как CommandEvents, WindowEvents и т. Д.), На верхний поля уровня нашего основного пакета подкласса. Причина этого заключается в том, что в случае, когда обработчик назначается через локальную переменную уровня функции, он теряется сразу после выхода из метода. Такое поведение, вероятно, можно отнести к сборщику мусора .NET, хотя мы получили эти ссылки из интерфейса DTE, который определенно существует в течение всего времени существования нашего пакета расширения.
Обработка событий проекта и решения для расширений VSPackage
Давайте рассмотрим некоторые интерфейсы из пространства имен Microsoft.VisualStudio.Shell.Interop — те, которые позволяют нам более точно обрабатывать события, связанные с проектами и решениями Visual Studio. Хотя эти интерфейсы не являются частью модели автоматизации EnvDTE, они могут быть реализованы основным классом расширения VSPackage (то есть классом, унаследованным от базового класса Package в Managed Package Framework). Вот почему, если вы разрабатываете расширение этого типа, эти интерфейсы удобно дополняют базовый набор интерфейсов, предоставляемых объектом DTE. Кстати, это еще один аргумент в пользу создания полноценного плагина VSPackage с использованием MPF.
В IVsSolutionEvents может быть реализован с помощью класса , унаследованного от пакета, и он доступен , начиная с версии Visual Studio 2005, и изолированного \ интегрированных оболочек приложений. Этот интерфейс позволяет отслеживать загрузку, выгрузку, открытие и закрытие проектов или даже целых решений в среде разработки путем реализации таких методов, как 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).
Такие обработчики событий должны пригодиться в случае, если вашему плагину необходимо выполнить некоторый код сразу после его инициализации, и в то же время плагин зависит от проектов \ решений, которые загружаются в IDE. В этом случае выполнение такого кода без ожидания завершения загрузки решения может привести к неверным (неполным) результатам из-за не полностью сформированного дерева проектов или даже к исключениям времени выполнения.
При разработке плагина IDE PVS-Studio мы столкнулись с еще одним интересным аспектом инициализации плагина VSPackage. Когда один плагин Package входит в состояние ожидания (например, путем отображения диалогового окна для пользователя), дальнейшая инициализация расширений VSPackage приостанавливается до тех пор, пока не вернется блокирующий плагин. Таким образом, при обработке загрузки и инициализации внутри среды всегда следует помнить и об этом возможном сценарии.
И, наконец, я хочу в последний раз вернуться к тому факту, что для правильной работы описанных выше методов интерфейса вы должны наследовать свой основной класс от этих интерфейсов:
class MyPackage: Package, IVsSolutionLoadEvents, IVsSolutionEvents { //Implementation of Package, IVsSolutionLoadEvents, IVsSolutionEvents ... }
Поддержка цветовых схем Visual Studio
Если разрабатываемое расширение будет интегрировано в интерфейс среды разработки, например, путем создания пользовательских окон инструментов или окон документов MDI (и наиболее удобным способом такой интеграции является расширение VSPackage), рекомендуется, чтобы Цветовая гамма компонентов пользовательского интерфейса должна соответствовать общей цветовой схеме, используемой самой Visual Studio.
Важность этой задачи была повышена с выпуском Visual Studio 2012, содержащим две совершенно противоположные цветовые темы (Dark и Light), которые пользователь мог переключать «на лету» из окна параметров IDE.
Метод GetVSSysColorEx из интерфейса Visual Studio Interop IVsUIShell2 можно использовать для получения настроек цвета среды. Этот интерфейс доступен только для плагинов 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 для «рисования» наших пользовательских контекстных меню. Чтобы определить момент времени, когда цветовая тема ваших компонентов должна быть повторно применена, вы можете, например, использовать события команды среды, отвечающей за открытие окна настроек IDE (Инструменты -> Параметры). Как подписать ваши обработчики на такое событие было описано ранее в этой статье.
Но если по какой-то причине вы не можете использовать объект IVsUIShell2 (например, если вы разрабатываете расширение, отличное от VSPackage), но в то же время вам по-прежнему необходимо поддерживать цветовые темы Visual Studio, это возможно получить значения цвета для различных компонентов пользовательского интерфейса вашей среды непосредственно из системного реестра. Мы не будем рассматривать этот подход в статье, но здесь вы можете скачать бесплатный инструмент с открытым исходным кодом, предназначенный для редактирования цветовых тем Visual Studio. Инструмент написан на C # и содержит весь код, необходимый для чтения и изменения цветовых тем Visual Studio 2012 из управляемого кода.
Взаимодействие с интерфейсами COM из многопоточного приложения
Первоначально пакет расширения PVS-Studio не содержал каких-либо специальных механизмов обеспечения безопасности потоков для взаимодействия с API-интерфейсами Visual Studio. В то же время мы пытались ограничить взаимодействие с этими API-интерфейсами в одном фоновом потоке, который был создан и принадлежит нашему плагину. И такой подход функционировал безупречно в течение достаточно длительного периода. Тем не менее, несколько отчетов об ошибках от наших пользователей, каждый из которых содержал аналогичную ошибку ComExeption, побудили нас рассмотреть эту проблему более подробно и реализовать механизм безопасности многопоточности для нашего COM-взаимодействия.
Хотя модель автоматизации Visual Studio не является поточно-ориентированной, она все же обеспечивает способ взаимодействия с многопоточными приложениями. Приложение Visual Studio представляет собой сервер COM (объектный режим компонентов). Для задачи обработки вызовов от клиентов COM (в нашем случае это будет наш пакет расширения) к небезопасным серверам, COM предоставляет механизм, известный как модель STA (однопоточная квартира). В терминах COM квартира представляет собой логический контейнер внутри процесса, в котором объекты и потоки используют одни и те же правила доступа к потокам. STA может содержать только один поток, но неограниченное количество объектов внутри такого контейнера. Вызовы из других потоков в такие небезопасные объекты внутри STA преобразуются в сообщения и отправляются в очередь сообщений.Сообщения извлекаются из очереди сообщений и преобразуются обратно в вызовы методов по одному потоком, выполняющимся в STA, поэтому становится возможным только один поток для доступа к этим небезопасным объектам на сервере.
Использование квартирного механизма внутри управляемого кода
.NET Framework не использует механику COM-квартир напрямую. Поэтому, когда управляемое приложение вызывает COM-объект в сценариях взаимодействия COM, CLR (Common Language Runtime) создает и инициализирует контейнер квартиры. Управляемый поток может создавать и вводить либо MTA (многопоточная квартира, контейнер, который, в отличие от STA, может одновременно содержать несколько потоков), либо STA, хотя поток будет запущен как MTA по умолчанию. Тип квартиры может быть указан до запуска потока:
Thread t = new Thread(ThreadProc); t.SetApartmentState(ApartmentState.STA); ... t.Start();
Поскольку тип квартиры не может быть изменен после запуска потока, атрибут STAThread должен использоваться для указания основного потока управляемого приложения в качестве STA:
[STAThread] static void Main(string[] args) {...}
Реализация фильтров сообщений для ошибок взаимодействия COM в управляемой среде
STA сериализует все вызовы на COM-сервер, поэтому один из вызывающих клиентов может быть потенциально заблокирован или даже отклонен, когда сервер занят, обрабатывает различные вызовы или другой поток уже находится внутри контейнера квартиры. В случае отклонения COM-сервером своего клиента .NET COM-взаимодействие создаст исключение System.Runtime.InteropServices.COME («Фильтр сообщений указал, что приложение занято»).
При работе с модулем Visual Studio (надстройка, vspackage) или макросом управление выполнением обычно передается в модуль из основного потока пользовательского интерфейса STA среды (например, в случае обработки событий или изменений состояния среды и т. Д.) , Вызов интерфейсов автоматизации COM из этого основного потока IDE является безопасным. Но если планируется использовать другие фоновые потоки и интерфейсы COM EnvDTE должны вызываться из этих фоновых потоков (как в случае длинных вычислений, которые могут потенциально повредить интерфейс IDE, если они выполняются в основном потоке пользовательского интерфейса), то Рекомендуется реализовать механизм обработки вызовов, отклоненных сервером.
Работая над плагином PVS-Studio, мы часто сталкивались с подобными исключениями COM в ситуациях, когда другие сторонние расширения были активны внутри IDE одновременно с плагином PVS-Studio. Тяжелое взаимодействие пользователя с пользовательским интерфейсом также было обычной причиной таких проблем. Вполне логично, что эти ситуации часто приводили к одновременным параллельным вызовам COM-объектов внутри STA и, следовательно, к отклонению некоторых из них.
Для выборочной обработки входящих и исходящих вызовов COM предоставляет интерфейс IMessageFilter. Если сервер реализует его, все вызовы передаются методу HandleIncomingCall, и клиент информируется об отклоненных вызовах через метод RetryRejectedCall. Это, в свою очередь, позволяет повторять отклоненные вызовы или, по крайней мере, правильно представлять этот отказ пользователю (например, путем отображения диалога с сообщением «сервер занят»). Ниже приведен пример реализации обработки отклоненных вызовов для управляемого приложения:
[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 ... }
Рекомендации
- MSDN. Ссылки на сборки автоматизации и объект DTE2.
- MSDN. Функциональные группы автоматизации.
- MZ-Tools. HOWTO: правильно используйте метод OnConnection надстройки Visual Studio.
- Кодовый проект. Понимание COM-однопоточной квартиры.
- MZ-Tools. HOWTO: Добавьте обработчик событий из надстройки Visual Studio .
- Блог доктора Экс. Использование EnableVSIPLogging для идентификации меню и команд с VS 2005 + SP1.