>
>
Команды Visual Studio

Павел Еремеев
Статей: 38

Команды Visual Studio

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

Эта статья устарела. Обновленную версию этой статьи вы можете прочитать здесь.

Введение

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

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

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

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

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

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

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

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

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

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

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

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

>alias MyAlias File.NewFile MyFile

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      ...
  }

}

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

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

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

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

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

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

MyCommand1.Delete();

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

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

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

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

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

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

Другие статьи этого цикла