Поделиться через


Пошаговое руководство. Создание украшения представления, команд и параметров (направляющие столбцов)

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

В этом пошаговом руководстве описаны следующие операции:

  • Создание проекта VSIX

  • Добавление декора представления редактора

  • Добавьте поддержку сохранения и получения параметров (где нарисовать направляющие столбцов и их цвет)

  • Добавление команд (добавление и удаление направляющих столбцов, изменение цвета)

  • Поместите команды в меню "Изменение" и контекстных меню текстового документа

  • Добавление поддержки вызова команд из командного окна Visual Studio

    Вы можете попробовать версию направляющих столбцов с помощью этого расширения коллекции Visual Studio.

    Примечание.

    В этом пошаговом руководстве вы вставьте большой объем кода в несколько файлов, созданных шаблонами расширений Visual Studio. Но вскоре это пошаговое руководство будет ссылаться на готовое решение на GitHub с другими примерами расширений. Завершенный код немного отличается от того, что он имеет реальные значки команд вместо использования значков generictemplate.

Настройка решения

Сначала вы создадите проект VSIX, добавьте украшение представления редактора, а затем добавьте команду (которая добавляет VSPackage для владения командой). Базовая архитектура выглядит следующим образом:

  • У вас есть прослушиватель создания текстового ColumnGuideAdornment представления, который создает объект для каждого представления. Этот объект прослушивает события об изменении представления или изменении параметров, обновлении или перерисовки направляющих столбцов при необходимости.

  • Существует функция GuidesSettingsManager , которая обрабатывает чтение и запись из хранилища параметров Visual Studio. Диспетчер параметров также имеет операции обновления параметров, поддерживающих пользовательские команды (добавление столбца, удаление столбца, изменение цвета).

  • Существует пакет VSIP, который необходим, если у вас есть пользовательские команды, но это просто стандартный код, который инициализирует объект реализации команд.

  • ColumnGuideCommands Существует объект, который запускает пользовательские команды и подключает обработчики команд для команд, объявленных в VSCT-файле.

    VSIX. Использование файла | Новые функции... команда для создания проекта. Выберите узел расширяемости в C# в области навигации слева и выберите проект VSIX в правой области. Введите имя ColumnGuides и нажмите кнопку "ОК ", чтобы создать проект.

    Вид украшений. Нажмите правую кнопку указателя на узле проекта в Обозреватель решений. Нажмите кнопку "Добавить" | Новый элемент ... команда для добавления нового элемента оформления представления. Выбор расширяемости | Редактор в левой области навигации и выбор элемента " Конструктор представления окна" в правой области. Введите имя ColumnGuideAdornment в качестве имени элемента и нажмите кнопку "Добавить ", чтобы добавить его.

    Этот шаблон элемента добавлен в проект (а также ссылки и т. д.): ColumnGuideAdornment.cs и ColumnGuideAdornmentTextViewCreationListener.cs. Шаблоны рисуют фиолетовый прямоугольник в представлении. В следующем разделе вы измените пару строк в прослушивателе создания представления и замените содержимое ColumnGuideAdornment.cs.

    Команды. В Обозреватель решений нажмите правую кнопку указателя на узле проекта. Нажмите кнопку "Добавить" | Новый элемент ... команда для добавления нового элемента оформления представления. Выбор расширяемости | VSPackage в области навигации слева и выберите "Настраиваемая команда " в правой области. Введите имя ColumnGuideCommands в качестве имени элемента и нажмите кнопку "Добавить". Помимо нескольких ссылок, добавление команд и пакетов также добавило ColumnGuideCommands.cs, ColumnGuideCommandsPackage.cs и ColumnGuideCommandsPackage.vsct. В следующем разделе вы замените содержимое первых и последних файлов для определения и реализации команд.

Настройка прослушивателя создания представления текста

Откройте ColumnGuideAdornmentTextViewCreationListener.cs в редакторе. Этот код реализует обработчик всякий раз, когда Visual Studio создает текстовые представления. Существуют атрибуты, управляющие вызовом обработчика в зависимости от характеристик представления.

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

[Order(After = PredefinedAdornmentLayers.Caret)]

с этими двумя строками:

[Order(Before = PredefinedAdornmentLayers.Text)]
[TextViewRole(PredefinedTextViewRoles.Document)]

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

Реализация диспетчера параметров

Замените содержимое GuidesSettingsManager.cs следующим кодом (описано ниже):

using Microsoft.VisualStudio.Settings;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Settings;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media;

namespace ColumnGuides
{
    internal static class GuidesSettingsManager
    {
        // Because my code is always called from the UI thred, this succeeds.
        internal static SettingsManager VsManagedSettingsManager =
            new ShellSettingsManager(ServiceProvider.GlobalProvider);

        private const int _maxGuides = 5;
        private const string _collectionSettingsName = "Text Editor";
        private const string _settingName = "Guides";
        // 1000 seems reasonable since primary scenario is long lines of code
        private const int _maxColumn = 1000;

        static internal bool AddGuideline(int column)
        {
            if (! IsValidColumn(column))
                throw new ArgumentOutOfRangeException(
                    "column",
                    "The parameter must be between 1 and " + _maxGuides.ToString());
            var offsets = GuidesSettingsManager.GetColumnOffsets();
            if (offsets.Count() >= _maxGuides)
                return false;
            // Check for duplicates
            if (offsets.Contains(column))
                return false;
            offsets.Add(column);
            WriteSettings(GuidesSettingsManager.GuidelinesColor, offsets);
            return true;
        }

        static internal bool RemoveGuideline(int column)
        {
            if (!IsValidColumn(column))
                throw new ArgumentOutOfRangeException(
                    "column", "The parameter must be between 1 and 10,000");
            var columns = GuidesSettingsManager.GetColumnOffsets();
            if (! columns.Remove(column))
            {
                // Not present.  Allow user to remove the last column
                // even if they're not on the right column.
                if (columns.Count != 1)
                    return false;

                columns.Clear();
            }
            WriteSettings(GuidesSettingsManager.GuidelinesColor, columns);
            return true;
        }

        static internal bool CanAddGuideline(int column)
        {
            if (!IsValidColumn(column))
                return false;
            var offsets = GetColumnOffsets();
            if (offsets.Count >= _maxGuides)
                return false;
            return ! offsets.Contains(column);
        }

        static internal bool CanRemoveGuideline(int column)
        {
            if (! IsValidColumn(column))
                return false;
            // Allow user to remove the last guideline regardless of the column.
            // Okay to call count, we limit the number of guides.
            var offsets = GuidesSettingsManager.GetColumnOffsets();
            return offsets.Contains(column) || offsets.Count() == 1;
        }

        static internal void RemoveAllGuidelines()
        {
            WriteSettings(GuidesSettingsManager.GuidelinesColor, new int[0]);
        }

        private static bool IsValidColumn(int column)
        {
            // zero is allowed (per user request)
            return 0 <= column && column <= _maxColumn;
        }

        // This has format "RGB(<int>, <int>, <int>) <int> <int>...".
        // There can be any number of ints following the RGB part,
        // and each int is a column (char offset into line) where to draw.
        static private string _guidelinesConfiguration;
        static private string GuidelinesConfiguration
        {
            get
            {
                if (_guidelinesConfiguration == null)
                {
                    _guidelinesConfiguration =
                        GetUserSettingsString(
                            GuidesSettingsManager._collectionSettingsName,
                            GuidesSettingsManager._settingName)
                        .Trim();
                }
                return _guidelinesConfiguration;
            }

            set
            {
                if (value != _guidelinesConfiguration)
                {
                    _guidelinesConfiguration = value;
                    WriteUserSettingsString(
                        GuidesSettingsManager._collectionSettingsName,
                        GuidesSettingsManager._settingName, value);
                    // Notify ColumnGuideAdornments to update adornments in views.
                    var handler = GuidesSettingsManager.SettingsChanged;
                    if (handler != null)
                        handler();
                }
            }
        }

        internal static string GetUserSettingsString(string collection, string setting)
        {
            var store = GuidesSettingsManager
                            .VsManagedSettingsManager
                            .GetReadOnlySettingsStore(SettingsScope.UserSettings);
            return store.GetString(collection, setting, "RGB(255,0,0) 80");
        }

        internal static void WriteUserSettingsString(string key, string propertyName,
                                                     string value)
        {
            var store = GuidesSettingsManager
                            .VsManagedSettingsManager
                            .GetWritableSettingsStore(SettingsScope.UserSettings);
            store.CreateCollection(key);
            store.SetString(key, propertyName, value);
        }

        // Persists settings and sets property with side effect of signaling
        // ColumnGuideAdornments to update.
        static private void WriteSettings(Color color, IEnumerable<int> columns)
        {
            string value = ComposeSettingsString(color, columns);
            GuidelinesConfiguration = value;
        }

        private static string ComposeSettingsString(Color color,
                                                    IEnumerable<int> columns)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("RGB({0},{1},{2})", color.R, color.G, color.B);
            IEnumerator<int> columnsEnumerator = columns.GetEnumerator();
            if (columnsEnumerator.MoveNext())
            {
                sb.AppendFormat(" {0}", columnsEnumerator.Current);
                while (columnsEnumerator.MoveNext())
                {
                    sb.AppendFormat(", {0}", columnsEnumerator.Current);
                }
            }
            return sb.ToString();
        }

        // Parse a color out of a string that begins like "RGB(255,0,0)"
        static internal Color GuidelinesColor
        {
            get
            {
                string config = GuidelinesConfiguration;
                if (!String.IsNullOrEmpty(config) && config.StartsWith("RGB("))
                {
                    int lastParen = config.IndexOf(')');
                    if (lastParen > 4)
                    {
                        string[] rgbs = config.Substring(4, lastParen - 4).Split(',');

                        if (rgbs.Length >= 3)
                        {
                            byte r, g, b;
                            if (byte.TryParse(rgbs[0], out r) &&
                                byte.TryParse(rgbs[1], out g) &&
                                byte.TryParse(rgbs[2], out b))
                            {
                                return Color.FromRgb(r, g, b);
                            }
                        }
                    }
                }
                return Colors.DarkRed;
            }

            set
            {
                WriteSettings(value, GetColumnOffsets());
            }
        }

        // Parse a list of integer values out of a string that looks like
        // "RGB(255,0,0) 1, 5, 10, 80"
        static internal List<int> GetColumnOffsets()
        {
            var result = new List<int>();
            string settings = GuidesSettingsManager.GuidelinesConfiguration;
            if (String.IsNullOrEmpty(settings))
                return new List<int>();

            if (!settings.StartsWith("RGB("))
                return new List<int>();

            int lastParen = settings.IndexOf(')');
            if (lastParen <= 4)
                return new List<int>();

            string[] columns = settings.Substring(lastParen + 1).Split(',');

            int columnCount = 0;
            foreach (string columnText in columns)
            {
                int column = -1;
                // VS 2008 gallery extension didn't allow zero, so per user request ...
                if (int.TryParse(columnText, out column) && column >= 0)
                {
                    columnCount++;
                    result.Add(column);
                    if (columnCount >= _maxGuides)
                        break;
                }
            }
            return result;
        }

        // Delegate and Event to fire when settings change so that ColumnGuideAdornments
        // can update.  We need nothing special in this event since the settings manager
        // is statically available.
        //
        internal delegate void SettingsChangedHandler();
        static internal event SettingsChangedHandler SettingsChanged;

    }
}

Большая часть этого кода создает и анализирует формат параметров: RGB(<int,int,int<><), <>int, <int>>>, ...". Целые числа в конце — это одноуровневые столбцы, в которых требуется направляющие столбцов. Расширение направляющих столбцов записывает все его параметры в одной строке значения.

Есть некоторые части кода, которые стоит выделить. Следующая строка кода получает управляемую оболочку Visual Studio для хранилища параметров. В большинстве случаев это абстрагируется по реестру Windows, но этот API не зависит от механизма хранения.

internal static SettingsManager VsManagedSettingsManager =
    new ShellSettingsManager(ServiceProvider.GlobalProvider);

Хранилище параметров Visual Studio использует идентификатор категории и идентификатор параметра для уникальной идентификации всех параметров:

private const string _collectionSettingsName = "Text Editor";
private const string _settingName = "Guides";

Вам не нужно использовать "Text Editor" имя категории. Вы можете выбрать все, что вам нравится.

Первые несколько функций — это точки входа для изменения параметров. Они проверяют ограничения высокого уровня, такие как максимально допустимое количество направляющих. Затем вызывается WriteSettings, который создает строку параметров и задает свойство GuideLinesConfiguration. Задание этого свойства сохраняет значение параметров в хранилище параметров Visual Studio и запускает SettingsChanged событие для обновления всех ColumnGuideAdornment объектов, связанных с текстовым представлением.

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

Реализация класса ColumnGuideAdornment

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

Замените содержимое ColumnGuideAdornment.cs следующим кодом (описано ниже):

using System;
using System.Windows.Media;
using Microsoft.VisualStudio.Text.Editor;
using System.Collections.Generic;
using System.Windows.Shapes;
using Microsoft.VisualStudio.Text.Formatting;
using System.Windows;

namespace ColumnGuides
{
    /// <summary>
    /// Adornment class, one instance per text view that draws a guides on the viewport
    /// </summary>
    internal sealed class ColumnGuideAdornment
    {
        private const double _lineThickness = 1.0;
        private IList<Line> _guidelines;
        private IWpfTextView _view;
        private double _baseIndentation;
        private double _columnWidth;

        /// <summary>
        /// Creates editor column guidelines
        /// </summary>
        /// <param name="view">The <see cref="IWpfTextView"/> upon
        /// which the adornment will be drawn</param>
        public ColumnGuideAdornment(IWpfTextView view)
        {
            _view = view;
            _guidelines = CreateGuidelines();
            GuidesSettingsManager.SettingsChanged +=
                new GuidesSettingsManager.SettingsChangedHandler(SettingsChanged);
            view.LayoutChanged +=
                new EventHandler<TextViewLayoutChangedEventArgs>(OnViewLayoutChanged);
            _view.Closed += new EventHandler(OnViewClosed);
        }

        void SettingsChanged()
        {
            _guidelines = CreateGuidelines();
            UpdatePositions();
            AddGuidelinesToAdornmentLayer();
        }

        void OnViewClosed(object sender, EventArgs e)
        {
            _view.LayoutChanged -= OnViewLayoutChanged;
            _view.Closed -= OnViewClosed;
            GuidesSettingsManager.SettingsChanged -= SettingsChanged;
        }

        private bool _firstLayoutDone;

        void OnViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
        {
            bool fUpdatePositions = false;

            IFormattedLineSource lineSource = _view.FormattedLineSource;
            if (lineSource == null)
            {
                return;
            }
            if (_columnWidth != lineSource.ColumnWidth)
            {
                _columnWidth = lineSource.ColumnWidth;
                fUpdatePositions = true;
            }
            if (_baseIndentation != lineSource.BaseIndentation)
            {
                _baseIndentation = lineSource.BaseIndentation;
                fUpdatePositions = true;
            }
            if (fUpdatePositions ||
                e.VerticalTranslation ||
                e.NewViewState.ViewportTop != e.OldViewState.ViewportTop ||
                e.NewViewState.ViewportBottom != e.OldViewState.ViewportBottom)
            {
                UpdatePositions();
            }
            if (!_firstLayoutDone)
            {
                AddGuidelinesToAdornmentLayer();
                _firstLayoutDone = true;
            }
        }

        private static IList<Line> CreateGuidelines()
        {
            Brush lineBrush = new SolidColorBrush(GuidesSettingsManager.GuidelinesColor);
            DoubleCollection dashArray = new DoubleCollection(new double[] { 1.0, 3.0 });
            IList<Line> result = new List<Line>();
            foreach (int column in GuidesSettingsManager.GetColumnOffsets())
            {
                Line line = new Line()
                {
                    // Use the DataContext slot as a cookie to hold the column
                    DataContext = column,
                    Stroke = lineBrush,
                    StrokeThickness = _lineThickness,
                    StrokeDashArray = dashArray
                };
                result.Add(line);
            }
            return result;
        }

        void UpdatePositions()
        {
            foreach (Line line in _guidelines)
            {
                int column = (int)line.DataContext;
                line.X2 = _baseIndentation + 0.5 + column * _columnWidth;
                line.X1 = line.X2;
                line.Y1 = _view.ViewportTop;
                line.Y2 = _view.ViewportBottom;
            }
        }

        void AddGuidelinesToAdornmentLayer()
        {
            // Grab a reference to the adornment layer that this adornment
            // should be added to
            // Must match exported name in ColumnGuideAdornmentTextViewCreationListener
            IAdornmentLayer adornmentLayer =
                _view.GetAdornmentLayer("ColumnGuideAdornment");
            if (adornmentLayer == null)
                return;
            adornmentLayer.RemoveAllAdornments();
            // Add the guidelines to the adornment layer and make them relative
            // to the viewport
            foreach (UIElement element in _guidelines)
                adornmentLayer.AddAdornment(AdornmentPositioningBehavior.OwnerControlled,
                                            null, null, element, null);
        }
    }

}

Экземпляры этого класса удерживают на связанном IWpfTextView и списке Line объектов, рисуемых в представлении.

Конструктор (вызываемая при ColumnGuideAdornmentTextViewCreationListener создании новых представлений Visual Studio) создает объекты руководства Line по столбцам. Конструктор также добавляет обработчики для SettingsChanged события (определенного в GuidesSettingsManager) и событий LayoutChanged представления и Closed.

Событие LayoutChanged запускается из-за нескольких видов изменений в представлении, в том числе при создании представления Visual Studio. Обработчик OnViewLayoutChanged вызывает AddGuidelinesToAdornmentLayer выполнение. Код определяет OnViewLayoutChanged , требуется ли обновлять позиции строк на основе таких изменений, как изменение размера шрифта, просмотр заготовок, горизонтальное прокрутка и т. д. Код в UpdatePositions этом случае приводит к рисованию между символами или сразу после столбца текста, который находится в смещении указанного символа в строке текста.

Всякий раз, когда параметры изменяют SettingsChanged функцию, просто воссоздает все Line объекты с любыми новыми параметрами. После задания позиций строки код удаляет все предыдущие Line объекты из ColumnGuideAdornment слоя украшений и добавляет новые.

Определение команд, меню и размещения меню

Существует многое для объявления команд и меню, размещения групп команд или меню в различных других меню и подключения обработчиков команд. В этом пошаговом руководстве показано, как команды работают в этом расширении, но дополнительные сведения см. в разделе "Расширение меню и команды".

Общие сведения о коде

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

Существует три части реализации команд: ColumnGuideCommandsPackage.cs, ColumnGuideCommandsPackage.vsct и ColumnGuideCommands.cs. Код, созданный шаблонами, помещает команду в меню "Сервис ", которое отображает диалоговое окно в качестве реализации. Вы можете посмотреть, как это реализовано в VSCT и ColumnGuideCommands.cs-файлах , так как это просто. Вы замените код в этих файлах ниже.

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

Общий шаблон команд

Команды в расширении "Направляющие столбцов" являются примером очень распространенного шаблона в Visual Studio. Вы помещаете связанные команды в группу и помещаете эту группу в главное меню, часто с параметром "<CommandFlag>CommandWellOnly</CommandFlag>", чтобы сделать команду невидимой. Помещая команды в главное меню ( например, правка), предоставляют им хорошие имена (например , Edit.AddColumnGuide), которые полезны для поиска команд при повторном назначении привязок ключей в параметрах инструментов. Это также полезно для получения завершения при вызове команд из командного окна.

Затем вы добавите группу команд в контекстные меню или вложенные меню, в которых пользователь будет использовать команды. Visual Studio рассматривает CommandWellOnly как флаг невидимости только для главного меню. При размещении той же группы команд в контекстном меню или вложенном меню отображаются команды.

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

VSCT-файл

VSCT-файл объявляет команды и где они идут, а также значки и т. д. Замените содержимое VSCT-файла следующим кодом (описано ниже):

<?xml version="1.0" encoding="utf-8"?>
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable" xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <!--  This is the file that defines the actual layout and type of the commands.
        It is divided in different sections (e.g. command definition, command
        placement, ...), with each defining a specific set of properties.
        See the comment before each section for more details about how to
        use it. -->

  <!--  The VSCT compiler (the tool that translates this file into the binary
        format that VisualStudio will consume) has the ability to run a preprocessor
        on the vsct file; this preprocessor is (usually) the C++ preprocessor, so
        it is possible to define includes and macros with the same syntax used
        in C++ files. Using this ability of the compiler here, we include some files
        defining some of the constants that we will use inside the file. -->

  <!--This is the file that defines the IDs for all the commands exposed by
      VisualStudio. -->
  <Extern href="stdidcmd.h"/>

  <!--This header contains the command ids for the menus provided by the shell. -->
  <Extern href="vsshlids.h"/>

  <!--The Commands section is where commands, menus, and menu groups are defined.
      This section uses a Guid to identify the package that provides the command
      defined inside it. -->
  <Commands package="guidColumnGuideCommandsPkg">
    <!-- Inside this section we have different sub-sections: one for the menus, another
    for the menu groups, one for the buttons (the actual commands), one for the combos
    and the last one for the bitmaps used. Each element is identified by a command id
    that is a unique pair of guid and numeric identifier; the guid part of the identifier
    is usually called "command set" and is used to group different command inside a
    logically related group; your package should define its own command set in order to
    avoid collisions with command ids defined by other packages. -->

    <!-- In this section you can define new menu groups. A menu group is a container for
         other menus or buttons (commands); from a visual point of view you can see the
         group as the part of a menu contained between two lines. The parent of a group
         must be a menu. -->
    <Groups>

      <!-- The main group is parented to the edit menu. All the buttons within the group
           have the "CommandWellOnly" flag, so they're actually invisible, but it means
           they get canonical names that begin with "Edit". Using placements, the group
           is also placed in the GuidesSubMenu group. -->
      <!-- The priority 0xB801 is chosen so it goes just after
           IDG_VS_EDIT_COMMANDWELL -->
      <Group guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
             priority="0xB801">
        <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_EDIT" />
      </Group>

      <!-- Group for holding the "Guidelines" sub-menu anchor (the item on the menu that
           drops the sub menu). The group is parented to
           the context menu for code windows. That takes care of most editors, but it's
           also placed in a couple of other windows using Placements -->
      <Group guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
             priority="0x0600">
        <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN" />
      </Group>

    </Groups>

    <Menus>
      <Menu guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" priority="0x1000"
            type="Menu">
        <Parent guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup" />
        <Strings>
          <ButtonText>&Column Guides</ButtonText>
        </Strings>
      </Menu>
    </Menus>

    <!--Buttons section. -->
    <!--This section defines the elements the user can interact with, like a menu command or a button
        or combo box in a toolbar. -->
    <Buttons>
      <!--To define a menu group you have to specify its ID, the parent menu and its
          display priority.
          The command is visible and enabled by default. If you need to change the
          visibility, status, etc, you can use the CommandFlag node.
          You can add more than one CommandFlag node e.g.:
              <CommandFlag>DefaultInvisible</CommandFlag>
              <CommandFlag>DynamicVisibility</CommandFlag>
          If you do not want an image next to your command, remove the Icon node or
          set it to <Icon guid="guidOfficeIcon" id="msotcidNoIcon" /> -->

      <Button guid="guidColumnGuidesCommandSet" id="cmdidAddColumnGuide"
              priority="0x0100" type="Button">
        <Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
        <Icon guid="guidImages" id="bmpPicAddGuide" />
        <CommandFlag>CommandWellOnly</CommandFlag>
        <CommandFlag>AllowParams</CommandFlag>
        <Strings>
          <ButtonText>&Add Column Guide</ButtonText>
        </Strings>
      </Button>

      <Button guid="guidColumnGuidesCommandSet" id="cmdidRemoveColumnGuide"
              priority="0x0101" type="Button">
        <Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
        <Icon guid="guidImages" id="bmpPicRemoveGuide" />
        <CommandFlag>CommandWellOnly</CommandFlag>
        <CommandFlag>AllowParams</CommandFlag>
        <Strings>
          <ButtonText>&Remove Column Guide</ButtonText>
        </Strings>
      </Button>

      <Button guid="guidColumnGuidesCommandSet" id="cmdidChooseGuideColor"
              priority="0x0103" type="Button">
        <Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
        <Icon guid="guidImages" id="bmpPicChooseColor" />
        <CommandFlag>CommandWellOnly</CommandFlag>
        <Strings>
          <ButtonText>Column Guide &Color...</ButtonText>
        </Strings>
      </Button>

      <Button guid="guidColumnGuidesCommandSet" id="cmdidRemoveAllColumnGuides"
              priority="0x0102" type="Button">
        <Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
        <CommandFlag>CommandWellOnly</CommandFlag>
        <Strings>
          <ButtonText>Remove A&ll Columns</ButtonText>
        </Strings>
      </Button>
    </Buttons>

    <!--The bitmaps section is used to define the bitmaps that are used for the
        commands.-->
    <Bitmaps>
      <!--  The bitmap id is defined in a way that is a little bit different from the
            others:
            the declaration starts with a guid for the bitmap strip, then there is the
            resource id of the bitmap strip containing the bitmaps and then there are
            the numeric ids of the elements used inside a button definition. An important
            aspect of this declaration is that the element id
            must be the actual index (1-based) of the bitmap inside the bitmap strip. -->
      <Bitmap guid="guidImages" href="Resources\ColumnGuideCommands.png"
              usedList="bmpPicAddGuide, bmpPicRemoveGuide, bmpPicChooseColor" />
    </Bitmaps>

  </Commands>

  <CommandPlacements>

    <!-- Define secondary placements for our groups -->

    <!-- Place the group containing the three commands in the sub-menu -->
    <CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
                      priority="0x0100">
      <Parent guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" />
    </CommandPlacement>

    <!-- The HTML editor context menu, for some reason, redefines its own groups
         so we need to place a copy of our context menu there too. -->
    <CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
                      priority="0x1001">
      <Parent guid="CMDSETID_HtmEdGrp" id="IDMX_HTM_SOURCE_HTML" />
    </CommandPlacement>

    <!-- The HTML context menu in Dev12 changed. -->
    <CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
                      priority="0x1001">
      <Parent guid="CMDSETID_HtmEdGrp_Dev12" id="IDMX_HTM_SOURCE_HTML_Dev12" />
    </CommandPlacement>

    <!-- Similarly for Script -->
    <CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
                      priority="0x1001">
      <Parent guid="CMDSETID_HtmEdGrp" id="IDMX_HTM_SOURCE_SCRIPT" />
    </CommandPlacement>

    <!-- Similarly for ASPX  -->
    <CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
                      priority="0x1001">
      <Parent guid="CMDSETID_HtmEdGrp" id="IDMX_HTM_SOURCE_ASPX" />
    </CommandPlacement>

    <!-- Similarly for the XAML editor context menu -->
    <CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
                      priority="0x0600">
      <Parent guid="guidXamlUiCmds" id="IDM_XAML_EDITOR" />
    </CommandPlacement>

  </CommandPlacements>

  <!-- This defines the identifiers and their values used above to index resources
       and specify commands. -->
  <Symbols>
    <!-- This is the package guid. -->
    <GuidSymbol name="guidColumnGuideCommandsPkg"
                value="{e914e5de-0851-4904-b361-1a3a9d449704}" />

    <!-- This is the guid used to group the menu commands together -->
    <GuidSymbol name="guidColumnGuidesCommandSet"
                value="{c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e}">
      <IDSymbol name="GuidesContextMenuGroup" value="0x1020" />
      <IDSymbol name="GuidesMenuItemsGroup" value="0x1021" />
      <IDSymbol name="GuidesSubMenu" value="0x1022" />
      <IDSymbol name="cmdidAddColumnGuide" value="0x0100" />
      <IDSymbol name="cmdidRemoveColumnGuide" value="0x0101" />
      <IDSymbol name="cmdidChooseGuideColor" value="0x0102" />
      <IDSymbol name="cmdidRemoveAllColumnGuides" value="0x0103" />
    </GuidSymbol>

    <GuidSymbol name="guidImages" value="{2C99F852-587C-43AF-AA2D-F605DE2E46EF}">
      <IDSymbol name="bmpPicAddGuide" value="1" />
      <IDSymbol name="bmpPicRemoveGuide" value="2" />
      <IDSymbol name="bmpPicChooseColor" value="3" />
    </GuidSymbol>

    <GuidSymbol name="CMDSETID_HtmEdGrp_Dev12"
                value="{78F03954-2FB8-4087-8CE7-59D71710B3BB}">
      <IDSymbol name="IDMX_HTM_SOURCE_HTML_Dev12" value="0x1" />
    </GuidSymbol>

    <GuidSymbol name="CMDSETID_HtmEdGrp" value="{d7e8c5e1-bdb8-11d0-9c88-0000f8040a53}">
      <IDSymbol name="IDMX_HTM_SOURCE_HTML" value="0x33" />
      <IDSymbol name="IDMX_HTM_SOURCE_SCRIPT" value="0x34" />
      <IDSymbol name="IDMX_HTM_SOURCE_ASPX" value="0x35" />
    </GuidSymbol>

    <GuidSymbol name="guidXamlUiCmds" value="{4c87b692-1202-46aa-b64c-ef01faec53da}">
      <IDSymbol name="IDM_XAML_EDITOR" value="0x103" />
    </GuidSymbol>
  </Symbols>

</CommandTable>

GUID. Чтобы Visual Studio найти обработчики команд и вызвать их, необходимо убедиться, что GUID пакета, объявленный в файле ColumnGuideCommandsPackage.cs (созданном из шаблона элемента проекта), соответствует GUID пакета, объявленному в VSCT-файле (скопированном выше). Если вы повторно используете этот пример кода, убедитесь, что у вас есть другой GUID, чтобы не конфликтовать с кем-либо, кто, возможно, скопировал этот код.

Найдите эту строку в ColumnGuideCommandsPackage.cs и скопируйте GUID из кавычки:

public const string PackageGuidString = "ef726849-5447-4f73-8de5-01b9e930f7cd";

Затем вставьте GUID в Symbols VSCT-файл, чтобы в объявлениях была указана следующая строка:

<GuidSymbol name="guidColumnGuideCommandsPkg"
            value="{ef726849-5447-4f73-8de5-01b9e930f7cd}" />

Идентификаторы GUID для набора команд и файл изображения растрового изображения должны быть уникальными для расширений.

<GuidSymbol name="guidColumnGuidesCommandSet"
            value="{c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e}">
<GuidSymbol name="guidImages" value="{2C99F852-587C-43AF-AA2D-F605DE2E46EF}">

Но вам не нужно изменять набор команд и графические идентификаторы изображений растрового изображения в этом пошаговом руководстве, чтобы получить код для работы. Guid набора команд должен соответствовать объявлению в файле ColumnGuideCommands.cs , но вы также замените содержимое этого файла, поэтому идентификаторы GUID будут совпадать.

Другие идентификаторы GUID в VSCT-файле определяют существующие меню, в которые добавляются команды руководства по столбцу, поэтому они никогда не изменяются.

Разделы файлов. VSCT содержит три внешних раздела: команды, размещение и символы. В разделе команд определяются группы команд, меню, кнопки или элементы меню и растровые изображения для значков. В разделе размещения объявляется, где группы переходят в меню или дополнительные размещения в предварительно существующих меню. В разделе символов объявляются идентификаторы, используемые в другом месте vsct-файла, что делает vsct-код более читаемым, чем идентификаторы GUID и шестнадцатеричные числа везде.

Разделы "Команды", определения групп. В разделе команд сначала определяются группы команд. Группы команд — это команды, которые отображаются в меню с небольшими серыми линиями, разделяющими группы. Группа также может заполнить весь вложенный меню, как в этом примере, и в этом примере не отображаются серые линии разделения. VSCT-файлы объявляют две группы, GuidesMenuItemsGroup родительские IDM_VS_MENU_EDIT элементы в меню (главное меню редактирования) и GuidesContextMenuGroup родительские IDM_VS_CTXT_CODEWIN элементы в контекстном меню редактора кода.

Второе объявление группы имеет 0x0600 приоритет:

<Group guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
             priority="0x0600">

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

Раздел команд, определение меню. Затем раздел команд определяет вложенное меню GuidesSubMenu, родительское для этого GuidesContextMenuGroupэлемента. Это GuidesContextMenuGroup группа, добавляемая ко всем соответствующим контекстным меню. В разделе размещения код помещает группу с командами из четырех столбцов в этом подменю.

Раздел "Команды", определения кнопок. Затем раздел команд определяет элементы меню или кнопки, которые являются командами с четырьмя столбцами. CommandWellOnly, описанное выше, означает, что команды невидимы при размещении в главном меню. Два объявления кнопки меню (добавление руководства и удаление руководства) также имеют AllowParams флаг:

<CommandFlag>AllowParams</CommandFlag>

Этот флаг позволяет, а также размещение основного меню, команда для получения аргументов при вызове обработчика команд Visual Studio. Если пользователь выполняет команду из командного окна, аргумент передается обработчику команд в аргументах события.

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

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

<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
                  priority="0x0100">
  <Parent guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" />
</CommandPlacement>

Все остальные размещения добавляют GuidesContextMenuGroup (который содержит GuidesSubMenu) в другие контекстные меню редактора. Когда код объявил, GuidesContextMenuGroupон был родительским в контекстном меню редактора кода. Именно поэтому вы не видите размещение контекстного меню редактора кода.

Раздел символов. Как упоминалось выше, раздел символов объявляет идентификаторы, используемые в другом месте vsct-файла, что делает vsct-код более читаемым, чем идентификаторы GUID и шестнадцатеричные числа везде. Важными моментами этого раздела является то, что GUID пакета должен согласиться с объявлением в классе пакета. И guid набора команд должен согласиться с объявлением в классе реализации команды.

Реализация команд

Файл ColumnGuideCommands.cs реализует команды и подключает обработчики. Когда Visual Studio загружает пакет и инициализирует его, пакет в свою очередь вызывает Initialize класс реализации команд. Команды инициализации просто создают экземпляр класса, а конструктор подключает все обработчики команд.

Замените содержимое файла ColumnGuideCommands.cs следующим кодом (описано ниже):

using System;
using System.ComponentModel.Design;
using System.Globalization;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio;

namespace ColumnGuides
{
    /// <summary>
    /// Command handler
    /// </summary>
    internal sealed class ColumnGuideCommands
    {

        const int cmdidAddColumnGuide = 0x0100;
        const int cmdidRemoveColumnGuide = 0x0101;
        const int cmdidChooseGuideColor = 0x0102;
        const int cmdidRemoveAllColumnGuides = 0x0103;

        /// <summary>
        /// Command menu group (command set GUID).
        /// </summary>
        static readonly Guid CommandSet =
            new Guid("c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e");

        /// <summary>
        /// VS Package that provides this command, not null.
        /// </summary>
        private readonly Package package;

        OleMenuCommand _addGuidelineCommand;
        OleMenuCommand _removeGuidelineCommand;

        /// <summary>
        /// Initializes the singleton instance of the command.
        /// </summary>
        /// <param name="package">Owner package, not null.</param>
        public static void Initialize(Package package)
        {
            Instance = new ColumnGuideCommands(package);
        }

        /// <summary>
        /// Gets the instance of the command.
        /// </summary>
        public static ColumnGuideCommands Instance
        {
            get;
            private set;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ColumnGuideCommands"/> class.
        /// Adds our command handlers for menu (commands must exist in the command
        /// table file)
        /// </summary>
        /// <param name="package">Owner package, not null.</param>
        private ColumnGuideCommands(Package package)
        {
            if (package == null)
            {
                throw new ArgumentNullException("package");
            }

            this.package = package;

            // Add our command handlers for menu (commands must exist in the .vsct file)

            OleMenuCommandService commandService =
                this.ServiceProvider.GetService(typeof(IMenuCommandService))
                    as OleMenuCommandService;
            if (commandService != null)
            {
                // Add guide
                _addGuidelineCommand =
                    new OleMenuCommand(AddColumnGuideExecuted, null,
                                       AddColumnGuideBeforeQueryStatus,
                                       new CommandID(ColumnGuideCommands.CommandSet,
                                                     cmdidAddColumnGuide));
                _addGuidelineCommand.ParametersDescription = "<column>";
                commandService.AddCommand(_addGuidelineCommand);
                // Remove guide
                _removeGuidelineCommand =
                    new OleMenuCommand(RemoveColumnGuideExecuted, null,
                                       RemoveColumnGuideBeforeQueryStatus,
                                       new CommandID(ColumnGuideCommands.CommandSet,
                                                     cmdidRemoveColumnGuide));
                _removeGuidelineCommand.ParametersDescription = "<column>";
                commandService.AddCommand(_removeGuidelineCommand);
                // Choose color
                commandService.AddCommand(
                    new MenuCommand(ChooseGuideColorExecuted,
                                    new CommandID(ColumnGuideCommands.CommandSet,
                                                  cmdidChooseGuideColor)));
                // Remove all
                commandService.AddCommand(
                    new MenuCommand(RemoveAllGuidelinesExecuted,
                                    new CommandID(ColumnGuideCommands.CommandSet,
                                                  cmdidRemoveAllColumnGuides)));
            }
        }

        /// <summary>
        /// Gets the service provider from the owner package.
        /// </summary>
        private IServiceProvider ServiceProvider
        {
            get
            {
                return this.package;
            }
        }

        private void AddColumnGuideBeforeQueryStatus(object sender, EventArgs e)
        {
            int currentColumn = GetCurrentEditorColumn();
            _addGuidelineCommand.Enabled =
                GuidesSettingsManager.CanAddGuideline(currentColumn);
        }

        private void RemoveColumnGuideBeforeQueryStatus(object sender, EventArgs e)
        {
            int currentColumn = GetCurrentEditorColumn();
            _removeGuidelineCommand.Enabled =
                GuidesSettingsManager.CanRemoveGuideline(currentColumn);
        }

        private int GetCurrentEditorColumn()
        {
            IVsTextView view = GetActiveTextView();
            if (view == null)
            {
                return -1;
            }

            try
            {
                IWpfTextView textView = GetTextViewFromVsTextView(view);
                int column = GetCaretColumn(textView);

                // Note: GetCaretColumn returns 0-based positions. Guidelines are 1-based
                // positions.
                // However, do not subtract one here since the caret is positioned to the
                // left of
                // the given column and the guidelines are positioned to the right. We
                // want the
                // guideline to line up with the current caret position. e.g. When the
                // caret is
                // at position 1 (zero-based), the status bar says column 2. We want to
                // add a
                // guideline for column 1 since that will place the guideline where the
                // caret is.
                return column;
            }
            catch (InvalidOperationException)
            {
                return -1;
            }
        }

        /// <summary>
        /// Find the active text view (if any) in the active document.
        /// </summary>
        /// <returns>The IVsTextView of the active view, or null if there is no active
        /// document or the
        /// active view in the active document is not a text view.</returns>
        private IVsTextView GetActiveTextView()
        {
            IVsMonitorSelection selection =
                this.ServiceProvider.GetService(typeof(IVsMonitorSelection))
                                                    as IVsMonitorSelection;
            object frameObj = null;
            ErrorHandler.ThrowOnFailure(
                selection.GetCurrentElementValue(
                    (uint)VSConstants.VSSELELEMID.SEID_DocumentFrame, out frameObj));

            IVsWindowFrame frame = frameObj as IVsWindowFrame;
            if (frame == null)
            {
                return null;
            }

            return GetActiveView(frame);
        }

        private static IVsTextView GetActiveView(IVsWindowFrame windowFrame)
        {
            if (windowFrame == null)
            {
                throw new ArgumentException("windowFrame");
            }

            object pvar;
            ErrorHandler.ThrowOnFailure(
                windowFrame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView, out pvar));

            IVsTextView textView = pvar as IVsTextView;
            if (textView == null)
            {
                IVsCodeWindow codeWin = pvar as IVsCodeWindow;
                if (codeWin != null)
                {
                    ErrorHandler.ThrowOnFailure(codeWin.GetLastActiveView(out textView));
                }
            }
            return textView;
        }

        private static IWpfTextView GetTextViewFromVsTextView(IVsTextView view)
        {

            if (view == null)
            {
                throw new ArgumentNullException("view");
            }

            IVsUserData userData = view as IVsUserData;
            if (userData == null)
            {
                throw new InvalidOperationException();
            }

            object objTextViewHost;
            if (VSConstants.S_OK
                   != userData.GetData(Microsoft.VisualStudio
                                                .Editor
                                                .DefGuidList.guidIWpfTextViewHost,
                                       out objTextViewHost))
            {
                throw new InvalidOperationException();
            }

            IWpfTextViewHost textViewHost = objTextViewHost as IWpfTextViewHost;
            if (textViewHost == null)
            {
                throw new InvalidOperationException();
            }

            return textViewHost.TextView;
        }

        /// <summary>
        /// Given an IWpfTextView, find the position of the caret and report its column
        /// number. The column number is 0-based
        /// </summary>
        /// <param name="textView">The text view containing the caret</param>
        /// <returns>The column number of the caret's position. When the caret is at the
        /// leftmost column, the return value is zero.</returns>
        private static int GetCaretColumn(IWpfTextView textView)
        {
            // This is the code the editor uses to populate the status bar.
            Microsoft.VisualStudio.Text.Formatting.ITextViewLine caretViewLine =
                textView.Caret.ContainingTextViewLine;
            double columnWidth = textView.FormattedLineSource.ColumnWidth;
            return (int)(Math.Round((textView.Caret.Left - caretViewLine.Left)
                                       / columnWidth));
        }

        /// <summary>
        /// Determine the applicable column number for an add or remove command.
        /// The column is parsed from command arguments, if present. Otherwise
        /// the current position of the caret is used to determine the column.
        /// </summary>
        /// <param name="e">Event args passed to the command handler.</param>
        /// <returns>The column number. May be negative to indicate the column number is
        /// unavailable.</returns>
        /// <exception cref="ArgumentException">The column number parsed from event args
        /// was not a valid integer.</exception>
        private int GetApplicableColumn(EventArgs e)
        {
            var inValue = ((OleMenuCmdEventArgs)e).InValue as string;
            if (!string.IsNullOrEmpty(inValue))
            {
                int column;
                if (!int.TryParse(inValue, out column) || column < 0)
                    throw new ArgumentException("Invalid column");
                return column;
            }

            return GetCurrentEditorColumn();
        }

        /// <summary>
        /// This function is the callback used to execute a command when a menu item
        /// is clicked. See the Initialize method to see how the menu item is associated
        /// to this function using the OleMenuCommandService service and the MenuCommand
        /// class.
        /// </summary>
        private void AddColumnGuideExecuted(object sender, EventArgs e)
        {
            int column = GetApplicableColumn(e);
            if (column >= 0)
            {
                GuidesSettingsManager.AddGuideline(column);
            }
        }

        private void RemoveColumnGuideExecuted(object sender, EventArgs e)
        {
            int column = GetApplicableColumn(e);
            if (column >= 0)
            {
                GuidesSettingsManager.RemoveGuideline(column);
            }
        }

        private void RemoveAllGuidelinesExecuted(object sender, EventArgs e)
        {
            GuidesSettingsManager.RemoveAllGuidelines();
        }

        private void ChooseGuideColorExecuted(object sender, EventArgs e)
        {
            System.Windows.Media.Color color = GuidesSettingsManager.GuidelinesColor;

            using (System.Windows.Forms.ColorDialog picker =
                new System.Windows.Forms.ColorDialog())
            {
                picker.Color = System.Drawing.Color.FromArgb(255, color.R, color.G,
                                                             color.B);
                if (picker.ShowDialog() == System.Windows.Forms.DialogResult.OK)
                {
                    GuidesSettingsManager.GuidelinesColor =
                        System.Windows.Media.Color.FromRgb(picker.Color.R,
                                                           picker.Color.G,
                                                           picker.Color.B);
                }
            }
        }

    }
}

Исправление ссылок. На этом этапе отсутствует ссылка. Нажмите правую кнопку указателя на узле "Ссылки" в Обозреватель решений. Выберите команду Add ... (Добавить ... ). В диалоговом окне "Добавить ссылку" есть поле поиска в правом верхнем углу. Введите "редактор" (без двойных кавычки). Выберите элемент Microsoft.VisualStudio.Editor (необходимо установить флажок слева от элемента, а не просто выбрать элемент) и ОК, чтобы добавить ссылку.

Инициализация. При инициализации класса пакета он вызывает Initialize класс реализации команд. Инициализация ColumnGuideCommands создает экземпляр класса и сохраняет экземпляр класса и ссылку на пакет в членах класса.

Рассмотрим один из перехватчиков команд из конструктора классов:

_addGuidelineCommand =
    new OleMenuCommand(AddColumnGuideExecuted, null,
                       AddColumnGuideBeforeQueryStatus,
                       new CommandID(ColumnGuideCommands.CommandSet,
                                     cmdidAddColumnGuide));

Вы создаете OleMenuCommand. Visual Studio использует командную систему Microsoft Office. Ключевыми аргументами при создании экземпляра OleMenuCommand является функция, реализующая команду (AddColumnGuideExecuted), функция для вызова, когда Visual Studio отображает меню с командой (AddColumnGuideBeforeQueryStatus) и идентификатором команды. Visual Studio вызывает функцию состояния запроса перед отображением команды в меню, чтобы команда была невидимой или серой для определенного отображения меню (например, отключение копирования при отсутствии выбора), изменение его значка или даже изменение его имени (например, из "Добавить что-то" для удаления). и т. д. Идентификатор команды должен соответствовать идентификатору команды, объявленному в VSCT-файле . Строки для набора команд и командные руководства столбцов должны совпадать между VSCT-файлом и ColumnGuideCommands.cs.

Следующая строка предоставляет помощь при вызове команды с помощью командного окна (описано ниже):

_addGuidelineCommand.ParametersDescription = "<column>";

Состояние запроса. Функции AddColumnGuideBeforeQueryStatus состояния запроса и RemoveColumnGuideBeforeQueryStatus проверка некоторых параметров (например, максимальное количество направляющих или максимальный столбец) или если существует руководство по удалению столбцов. Они позволяют выполнять команды, если условия правильны. Функции состояния запроса должны быть эффективными, так как они выполняются каждый раз, когда Visual Studio отображает меню и для каждой команды в меню.

Функция AddColumnGuideExecuted. Интересная часть добавления руководства заключается в том, чтобы выяснить текущее представление редактора и расположение курсора. Во-первых, эта функция вызывает GetApplicableColumnвызов, который проверяет наличие предоставленного пользователем аргумента в аргументах события обработчика команд, и если нет, функция проверяет представление редактора:

private int GetApplicableColumn(EventArgs e)
{
    var inValue = ((OleMenuCmdEventArgs)e).InValue as string;
    if (!string.IsNullOrEmpty(inValue))
    {
        int column;
        if (!int.TryParse(inValue, out column) || column < 0)
            throw new ArgumentException("Invalid column");
        return column;
    }

    return GetCurrentEditorColumn();
}

GetCurrentEditorColumn нужно немного покопаться, чтобы получить IWpfTextView представление кода. Если вы выполняете трассировку GetActiveTextView, GetActiveViewи GetTextViewFromVsTextViewвы можете увидеть, как это сделать. Следующий код является соответствующим кодом, абстрактным, начиная с текущего выбора, а затем получение кадра в качестве объекта DocView IVsTextView, а затем получением из IVsTextView, а затем IVsUserData получением узла представления и, наконец, IWpfTextView:

   IVsMonitorSelection selection =
       this.ServiceProvider.GetService(typeof(IVsMonitorSelection))
           as IVsMonitorSelection;
   object frameObj = null;

ErrorHandler.ThrowOnFailure(selection.GetCurrentElementValue(
                                (uint)VSConstants.VSSELELEMID.SEID_DocumentFrame,
                                out frameObj));

   IVsWindowFrame frame = frameObj as IVsWindowFrame;
   if (frame == null)
       <<do nothing>>;

...
   object pvar;
   ErrorHandler.ThrowOnFailure(frame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView,
                                                  out pvar));

   IVsTextView textView = pvar as IVsTextView;
   if (textView == null)
   {
       IVsCodeWindow codeWin = pvar as IVsCodeWindow;
       if (codeWin != null)
       {
           ErrorHandler.ThrowOnFailure(codeWin.GetLastActiveView(out textView));
       }
   }

...
   if (textView == null)
       <<do nothing>>

   IVsUserData userData = textView as IVsUserData;
   if (userData == null)
       <<do nothing>>

   object objTextViewHost;
   if (VSConstants.S_OK
           != userData.GetData(Microsoft.VisualStudio.Editor.DefGuidList
                                                            .guidIWpfTextViewHost,
                                out objTextViewHost))
   {
       <<do nothing>>
   }

   IWpfTextViewHost textViewHost = objTextViewHost as IWpfTextViewHost;
   if (textViewHost == null)
       <<do nothing>>

   IWpfTextView textView = textViewHost.TextView;

После получения IWpfTextView можно получить столбец, в котором находится курсор:

private static int GetCaretColumn(IWpfTextView textView)
{
    // This is the code the editor uses to populate the status bar.
    Microsoft.VisualStudio.Text.Formatting.ITextViewLine caretViewLine =
        textView.Caret.ContainingTextViewLine;
    double columnWidth = textView.FormattedLineSource.ColumnWidth;
    return (int)(Math.Round((textView.Caret.Left - caretViewLine.Left)
                                / columnWidth));
}

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

Вызов команды из командного окна

Пример направляющих столбцов позволяет пользователям вызывать две команды из командного окна в виде расширяемости. Если вы используете представление | Другие окна | Команда Command Window отображается в командном окне . Вы можете взаимодействовать с командным окном, введя "edit"., а также с завершением имени команды и указанием аргумента 120, у вас есть следующий результат:

> Edit.AddColumnGuide 120
>

Фрагменты примера, которые включают это поведение, находятся в объявлениях VSCT-файла , ColumnGuideCommands конструктор класса при подключении обработчиков команд и реализации обработчика команд, которые проверяют аргументы событий.

Вы видели "<CommandFlag>CommandWellOnly</CommandFlag>" в VSCT-файле , а также размещения в главном меню "Изменить ", даже если команды не отображаются в пользовательском интерфейсе меню "Изменить ". Если они есть в главном меню "Изменить ", они содержат такие имена, как Edit.AddColumnGuide. Объявление группы команд, в котором содержатся четыре команды, помещают группу в меню "Изменить " напрямую:

<Group guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
             priority="0xB801">
        <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_EDIT" />
      </Group>

Раздел кнопок позже объявил команды CommandWellOnly , чтобы сохранить их невидимыми в главном меню и объявил их следующими AllowParams:

<Button guid="guidColumnGuidesCommandSet" id="cmdidAddColumnGuide"
        priority="0x0100" type="Button">
  <Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
  <Icon guid="guidImages" id="bmpPicAddGuide" />
  <CommandFlag>CommandWellOnly</CommandFlag>
  <CommandFlag>AllowParams</CommandFlag>

Вы видели код ColumnGuideCommands обработчика команд в конструкторе классов, который предоставил описание разрешенного параметра:

_addGuidelineCommand.ParametersDescription = "<column>";

Вы видели GetApplicableColumn проверку OleMenuCmdEventArgs значения функции перед проверкой представления редактора для текущего столбца:

private int GetApplicableColumn(EventArgs e)
{
    var inValue = ((OleMenuCmdEventArgs)e).InValue as string;
    if (!string.IsNullOrEmpty(inValue))
    {
        int column;
        if (!int.TryParse(inValue, out column) || column < 0)
            throw new ArgumentException("Invalid column");
        return column;
    }

Попробуйте расширение

Теперь можно нажать клавишу F5 , чтобы выполнить расширение "Направляющие столбцов". Откройте текстовый файл и используйте контекстное меню редактора, чтобы добавить строки руководства, удалить их и изменить цвет. Щелкните текст (не пробелы, переданные в конце строки), чтобы добавить руководство по столбцу, или редактор добавляет его в последний столбец в строке. Если вы используете командное окно и вызываете команды с аргументом, вы можете добавить направляющие столбцов в любом месте.

Если вы хотите попробовать различные размещения команд, изменить имена, изменить значки и т. д., и у вас возникли проблемы с Visual Studio с последним кодом в меню, можно сбросить экспериментальный куст, в котором выполняется отладка. Откройте меню "Пуск" Windows и введите "сброс". Найдите и выполните команду, сбросьте следующий экспериментальный экземпляр Visual Studio. Эта команда очищает экспериментальный куст реестра всех компонентов расширения. Он не очищает параметры от компонентов, поэтому при завершении работы экспериментального куста Visual Studio все еще есть, когда код считывает хранилище параметров при следующем запуске.

Завершенный проект кода

В ближайшее время будет проект GitHub примеров расширяемости Visual Studio, и завершенный проект будет там. Эта статья будет обновлена, чтобы указать там, когда это произойдет. Завершенный пример проекта может иметь разные guid и будет иметь другую полосу растровых карт для значков команд.

Вы можете попробовать версию направляющих столбцов с помощью этого расширения коллекции Visual Studio.