My pages

by Sergey Vinyarsky

Всем привет

Меня зовут Сергей Винярский. И я алкоголик программист. Последние годы разрабатывал большую десктопную штуку, связанную с бух. учетом, но пару месяцев назад нашел в себе силы порвать с ней окончательно, влившись в плотные ряды web-разработчиков в лице проекта Умные Маршруты 123route.ru. Проект пока молодой, можно сказать стартап (в хорошем смысле слова :)), но несомненно с большим будущим. Ура, товарищи.

Зачем эта страница? А черт его знает. Просто хотелось почту на этом домене иметь. Поэтому не могу обещать, что здесь будет много ценного контента, но все может быть. Интересуюсь я всем на свете, но работать приходится в основном с JS, C#, ASP.Net, MS SQL и иже с ними.

VSCT - продолжение

Продолжаем прошлый пост

Пример: Иконки в меню

Visual Studio поддерживает только изображения размером 16x16 и глубиной 8-bit (256 цветов) или 32-bit (TrueColor). Разумеется предпочтительно применять TrueColor. Обычно все изображения помещаются в один файл в формате bmp, gif или png. Иконки размещаются в одну линию по горизонтали, то есть общий размер изображения составит по вертикали 16 пикселей, а по горизонтали - 16 умноженное на количество иконок.

Добавим иконки к командам Package menu item 1 и Package menu item 2:

Я подготовил файл Icons16x16.png:

Результирующий VSCT приобрел следующий вид (убраны некоторые оставшиеся неизменными части для уменьшения объема):

Добавился элемент Bitmaps со ссылкой на файл изображения и требуемые ему константы в Symbols.

Не забудьте включить Icons16x16.png в ресурсы проекта.

Ссылка на иконку выполняются через GUID файла с изображениями и идентификатора конкретной иконки. Порядок перечисления идентификаторов в атрибуте usedList должен соответствовать порядку иконок в файле слева направо. Численные значения идентификаторов (pngCommand1 и pngCommand2) значения не имеют.

К огромному сожалению, добавление иконки к подменю через VSCT-файлы не поддерживается

Пример: Добавление тулбара

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

Единственное, что нам надо сделать - это указать, что наше подменю на самом деле не меню, а тулбар. Изменим значение атрибута type на Toolbar вместо Menu:

Поскольку все тулбары живут сами по себе и не могут “встраиваться” в родительские тулбары, мы не можем корректно заполнить элемент Parent, поэтому достаточно сослаться на себя же: идентификатор подменю MySubMenu и идентификатор его родителя тоже MySubMenu.

Дальше с нашим тулбаром можно работать как и с любым другим. Например указать VisualStudio показать команду в текстовом виде:

Пример: Добавление шорткатов

А если нам лень даже мышку подвинуть к тулбару, можно присвоить командам горячие комбинации клавиш. Добавим в файл элемент KeyBindings:

Здесь id - идентификаторы команд, editor - область действия нашего шортката. guidVSStd97 означает “глобально”, в любом режиме работы VisualStudio. В общем случае KeyBinding задается так:

Если шорткат “одинарный”, например, CTRL-C, то он задается в key1 и mod1, а key2 и mod2 опускаются. Ежели мы хотим задать составной шорткат, например, CTRL-E/CTRL-C (нажимаем CTRL-E и затем CTRL-C), то получится

К сожалению, или к счастью, если у какой-либо глобальной команды (у которой editor=“guidVSStd97”) уже задан такой же шорткат, какой хотим и мы, то студия наш запрос проигнорирует. В таком случае есть два выхода: просить пользователя присвоить шорткат нашей команде вручную через меню Customize…, либо перехватывать клавиатуру на низком уровне через IKeyProcessorProvider.

Пока все.

VSCT - как и с чем это едят

Как я и обещал в прошлый раз, давайте сегодня разберемся, каким же образом наше расширение может добавить команды и меню в Visual Studio. Студия, как правило, не загружает в память расширение до тех пор, пока оно не потребуется для выполнения какого-либо действия, например, пока пользователь не кликнет по пункту меню. Это означает, что студия отображает меню расширения до загрузки самого расширения. Как это реализовано? Данные меню хранятся в сборке как бинарный ресурс и студия берет информацию из него без загрузки всего остального. При компиляции расширения в этот ресурс помещается CTO-файл (Command Table Output), который в свою очередь является результатом компиляции VSCT-файла (Visual Studio Command Table). VSCT-компилятор - независимая от основного компилятора сущность, вызов которой мы должны в явном виде прописать в csProj-файл, как это показано ниже:

и

Мастер создания проекта Visual Studio Package делает это сам, если поставить одну из галочек, указывающих, что мы хотим добавить визуальные контролы:

VSCT-файл содержит XML-данные. Корневой элемент называется CommandTable:

Как вы видите, компания Microsoft предоставляет XSD-cхему, найти ее можно локально после установки Visual Studio SDK. На моей VS2013 она оказалась в C:\Program Files\Microsoft Visual Studio 12.0\XML\Schemas\VSCT.xsd

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

Обратите внимание, что указание пространства имен обязательно, иначе файл не скомпилируется. К тому же указание пространства имен включает в редакторе IntelliSence.

Элемент CommandTable в общем случае содержит следующие элементы:

Далеко не все элементы обязательны, некоторые из них выполняют свои, довольно узкие, задачи. Поэтому давайте лучше рассмотрим конкретные примеры и те элементы, которые для них потребуются.

Пример: Добавление команды в меню Edit

Добавим в меню Edit команду Package menu item.

Готовый VSCT-файл имеет следующий вид:

Из всех элементов нам понадобились только Extern, Commands и Symbols.

Элемент Symbols

Все меню, группы и команды идентифицируются по паре значений - guid и id, где id - целое 32-разрядное число. Поскольку человеку неудобно работать с числовыми значениями, каждому гуиду и каждому идентификатору присваиваются наименования. Эти соответствия прописываются вручную в Symbols в виде

Элементы IDSymbol вложены в GuidSymbol, поскольку целочисленные идентификаторы уникальны только в пределах GUID’а. Соответственно, у другого GUID’а могут быть такие же числовые идентификаторы. GUID’ы, очевидно, должны быть уникальны. Идентификаторы - любые значения, какие вам нравится, но уникальные в пределах GUID’а. Наименования тоже любые, для удобства обычно пишут разные префиксы, но это не обязательно. Элемент Symbols традиционно размещается в конце файла.

Элемент Commands

Здесь определяются все меню и команды. В нашем случае мы имеем один элемент Group и один элемент Button.

Button - это команда. Действительно, название сбивает с толку, поскольку команда совершенно не обязательно будет кнопкой. В нашем случае это пункт меню.

Если быть точнее, то команда - это опубликованное именованное действие. Это действие может быть вызвано другим расширением или набрано с клавиатуры в Command Window (попробуйте набрать там Edit.Packagemenuitem и нажать Enter, запустится наша команда). Мы для удобства пользователя привязываем к действию визуальный контрол, но в общем случае это не требуется.

Group (группа) - это смысловое объединение команд. Команды, входящие в каждую из групп, отделяются от остальных команд сепараторами. В нашем случае в группу входит только одна команда, поэтому мы получили один пункт меню с сепараторами сверху и снизу.

Взаимные отношения сущностей задаются через элементы Parent, то есть Родитель. Родителем Button прописан guid и id группы.

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

Здесь guidSHLMainMenu - GUID всех команд главного меню Visual Studio, а IDM_VS_MENU_EDIT идентификатор меню Edit. Откуда берутся эти константы? Из заголовочного файла stdidcmd.h, который мы включили в начале.

При необходимости найдите у себя этот файл, наименования определений там говорят сами за себя. На своей машине я обнаружил его в C:\Program Files (x86)\Microsoft Visual Studio 12.0\VSSDK\VisualStudioIntegration\Common\Inc

Значения Priority определяют местоположение данной команды или группы в группе или меню соответственно. Поиграйтесь с Priority в строке

Местоположение нашей группы с командой в меню Edit будет меняться. Чем меньше приоритет, тем обычно выше и левее отображается сущность.

Пример: Добавление подменю в меню Edit с двумя командами

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

Результирующий файл имеет следующий вид:

Появился элемент Menu. В нем задано наименование, под которым это подменю будет отображаться (элемент ButtonText) и родитель MyMenuGroup. Еще раз вспомним:

  • Команды входят в группу
  • Группа входит в меню
  • Меню входит в группу

То есть мы реализовали так:

  • В меню Edit помещаем группу MyMenuGroup
  • В группу MyMenuGroup помещаем меню MySubMenu
  • В меню MySubMenu помещаем группу MySubMenuGroup
  • В группу MySubMenuGroup помещаем команды cmdidMyCommand1 и cmdidMyCommand2

Казалось бы, зачем создавать MyMenuGroup, если в нем всего один элемент - наше меню, но правила жесткие, меню может быть только в группе. И команды тоже. Если командам напрямую прописать MySubMenu, то меню не отобразится вообще, так как с точки зрения Visual Studio оно вроде как пустое получается - без групп.

Пример: Добавление контекстного меню

До сих пор мы добавляли свои контролы к главному меню, однако совершенно очевидным образом мы можем расширить и контекстные меню в разных окнах. Давайте перенесем подменю с двумя командами из предыдущего примера в контекстное меню редактора кода. Для этого достаточно родителем MyMenuGroup сделать контекстное меню. То есть фрагмент

должен быть изменен на

И вот результат:

В следующий раз рассмотрим иконки, шорткаты и все остальное, что покажется важным. Спасибо за внимание. :)

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

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

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

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

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

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

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

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

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

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

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

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

и

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

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

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

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

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

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

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

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

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

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

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

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

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

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

VSBookmarks

Visual Studio несомненно очень крутой продукт, однако механизм закладок (bookmarks) явно не относится к его сильным сторонам. Посудите сами, куча одинаковых закладок, ходить по ним можно только последовательно вперед и назад. Хочешь перейти на конкретную закладку? Будь любезен открыть отдельное окошко и там мышкой выбрать. Ага, причем окошко выглядит как-то так

    ...\Source\AAA.cs     62 
    ...\Source\BBB.cs     130
    ...\Source\ССС.cs     287
    ...\Source\DDD.cs     315

Что было в строке 62? А что в 130? Непонятно. Надо перещелкивать по всем. Не знаю, у кого как, но я к третьей закладке уже забывал, что собственно хотел найти.

Короче, встречайте: плагин к студии, который добавляет в редактор нумерованные закладки. Работать с ними можно шорткатами: Ctrl-Shift-[1..9] - создать или перенести закладку с соответствующим номером, Ctrl-[1..9] - перейти на закладку. Закладки привязываются к строке кода, а не просто номеру строки. То есть нужный кусок кода не потеряется даже после редактирования.

Плагин можно найти в студии в списке Extensions and Updates (поищите VSBookmarks), либо собрать из исходников. Взять их можно на GitHub.