Procédure pas à pas : Créer un ornement d’affichage, des commandes et des paramètres (repères de colonnes)

Vous pouvez étendre l’éditeur de texte/code Visual Studio avec des commandes et des effets d’affichage. Cet article vous montre comment prendre en main une fonctionnalité d’extension populaire, des guides de colonnes. Les repères de colonnes sont des lignes visuellement claires dessinées sur la vue de l’éditeur de texte pour vous aider à gérer votre code sur des largeurs de colonne spécifiques. Plus précisément, le code mis en forme peut être important pour les exemples que vous incluez dans des documents, des billets de blog ou des rapports de bogues.

Lors de cette procédure pas à pas, vous allez effectuer les opérations suivantes :

  • Créer un projet VSIX

  • Ajouter un ornement d’affichage de l’éditeur

  • Ajouter la prise en charge de l’enregistrement et de l’obtention des paramètres (où dessiner des repères de colonne et leur couleur)

  • Ajouter des commandes (ajouter/supprimer des repères de colonne, modifier leur couleur)

  • Placer les commandes dans le menu Modifier et les menus contextuels du document texte

  • Ajouter la prise en charge de l’appel des commandes à partir de la fenêtre commande Visual Studio

    Vous pouvez essayer une version de la fonctionnalité guides de colonnes avec cette extension de galerieVisual Studio.

    Remarque

    Dans cette procédure pas à pas, vous collez une grande quantité de code dans quelques fichiers générés par les modèles d’extension Visual Studio. Mais bientôt, cette procédure pas à pas fait référence à une solution terminée sur GitHub avec d’autres exemples d’extension. Le code terminé est légèrement différent dans le fait qu’il a des icônes de commande réelles au lieu d’utiliser des icônes génériquestemplates.

Configurer la solution

Tout d’abord, vous créez un projet VSIX, ajoutez un ornement d’affichage d’éditeur, puis ajoutez une commande (qui ajoute un VSPackage pour posséder la commande). L’architecture de base est la suivante :

  • Vous disposez d’un écouteur de création d’affichage de texte qui crée un ColumnGuideAdornment objet par vue. Cet objet écoute les événements relatifs à la modification de l’affichage ou aux paramètres qui changent, la mise à jour ou le redessinage des repères de colonnes si nécessaire.

  • Il existe un GuidesSettingsManager qui gère la lecture et l’écriture à partir du stockage des paramètres Visual Studio. Le gestionnaire de paramètres dispose également d’opérations pour mettre à jour les paramètres qui prennent en charge les commandes utilisateur (ajouter une colonne, supprimer une colonne, modifier la couleur).

  • Il existe un package VSIP nécessaire si vous avez des commandes utilisateur, mais il s’agit simplement d’un code réutilisable qui initialise l’objet d’implémentation des commandes.

  • Il existe un ColumnGuideCommands objet qui exécute les commandes utilisateur et connecte les gestionnaires de commandes pour les commandes déclarées dans le fichier .vsct .

    VSIX. Utiliser un fichier | Nouveau... commande pour créer un projet. Choisissez le nœud d’extensibilité sous C# dans le volet de navigation gauche et choisissez Projet VSIX dans le volet droit. Entrez le nom ColumnGuides et choisissez OK pour créer le projet.

    Ornement de vue. Appuyez sur le bouton pointeur droit sur le nœud du projet dans le Explorateur de solutions. Choisir l’option Ajouter | Nouvel élément ... commande pour ajouter un nouvel élément d’ornement d’affichage. Choisir l’extensibilité | Éditeur dans le volet de navigation gauche et choisissez Ornement de la fenêtre d’affichage de l’éditeur dans le volet droit. Entrez le nom ColumnGuideAdornment comme nom de l’élément et choisissez Ajouter pour l’ajouter.

    Vous pouvez voir que ce modèle d’élément a ajouté deux fichiers au projet (ainsi que des références, et ainsi de suite) : ColumnGuideAdornment.cs et ColumnGuideAdornmentTextViewCreationListener.cs. Les modèles dessinent un rectangle violet sur la vue. Dans la section suivante, vous modifiez quelques lignes dans l’écouteur de création d’affichage et remplacez le contenu de ColumnGuideAdornment.cs.

    Commandes. Dans Explorateur de solutions, appuyez sur le bouton pointeur droit sur le nœud du projet. Choisir l’option Ajouter | Nouvel élément ... commande pour ajouter un nouvel élément d’ornement d’affichage. Choisir l’extensibilité | VSPackage dans le volet de navigation gauche et choisissez Commande personnalisée dans le volet droit. Entrez le nom ColumnGuideCommands comme nom de l’élément, puis choisissez Ajouter. Outre plusieurs références, l’ajout des commandes et du package a également ajouté ColumnGuideCommands.cs, ColumnGuideCommandsPackage.cs et ColumnGuideCommandsPackage.vsct. Dans la section suivante, vous remplacez le contenu des premiers et derniers fichiers pour définir et implémenter les commandes.

Configurer l’écouteur de création d’affichage de texte

Ouvrez ColumnGuideAdornmentTextViewCreationListener.cs dans l’éditeur. Ce code implémente un gestionnaire pour chaque fois que Visual Studio crée des vues de texte. Il existe des attributs qui contrôlent le moment où le gestionnaire est appelé en fonction des caractéristiques de la vue.

Le code doit également déclarer une couche d’ornement. Lorsque l’éditeur met à jour les vues, elle obtient les couches d’ornement de la vue et de celle-ci obtient les éléments d’ornement. Vous pouvez déclarer l’ordre de votre couche par rapport à d’autres avec des attributs. Remplacez la ligne suivante :

[Order(After = PredefinedAdornmentLayers.Caret)]

avec ces deux lignes :

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

La ligne que vous avez remplacée se trouve dans un groupe d’attributs qui déclarent une couche d’ornement. La première ligne que vous avez modifiée change uniquement lorsque les lignes de repère de colonne s’affichent. Dessiner les lignes « avant » le texte dans la vue signifie qu’elles apparaissent derrière ou sous le texte. La deuxième ligne déclare que les ornements du repère de colonne s’appliquent aux entités de texte qui correspondent à votre notion de document, mais vous pouvez déclarer l’ornement, par exemple, pour fonctionner uniquement pour du texte modifiable. Il existe plus d’informations dans les points d’extension du service de langage et de l’éditeur

Implémenter le gestionnaire de paramètres

Remplacez le contenu des guides Paramètres Manager.cs par le code suivant (expliqué ci-dessous) :

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 paramenter 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;

    }
}

La plupart de ce code crée et analyse le format des paramètres : « RVB(<int,int,int<<>>) <int>>, <int>, ... Les entiers à la fin sont les colonnes uni-basées sur lesquelles vous souhaitez des repères de colonnes. L’extension de repère de colonne capture tous ses paramètres dans une chaîne de valeur de paramètre unique.

Il existe certaines parties du code qui méritent d’être mises en surbrillance. La ligne de code suivante obtient le wrapper managé Visual Studio pour le stockage des paramètres. Dans la plupart des cas, cela est abstrait sur le Registre Windows, mais cette API est indépendante du mécanisme de stockage.

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

Le stockage des paramètres Visual Studio utilise un identificateur de catégorie et un identificateur de paramètre pour identifier de manière unique tous les paramètres :

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

Vous n’avez pas besoin d’utiliser "Text Editor" comme nom de catégorie. Vous pouvez choisir tout ce que vous aimez.

Les premières fonctions sont les points d’entrée pour modifier les paramètres. Ils case activée contraintes de haut niveau, comme le nombre maximal de guides autorisés. Ensuite, ils appellent WriteSettings, qui compose une chaîne de paramètres et définit la propriété GuideLinesConfiguration. La définition de cette propriété enregistre la valeur des paramètres dans le magasin de paramètres Visual Studio et déclenche l’événement SettingsChanged pour mettre à jour tous les ColumnGuideAdornment objets, chacun associé à un affichage texte.

Il existe quelques fonctions de point d’entrée, telles que CanAddGuideline, qui sont utilisées pour implémenter des commandes qui modifient les paramètres. Lorsque Visual Studio affiche des menus, il interroge les implémentations de commandes pour voir si la commande est actuellement activée, quel est son nom, et ainsi de suite. Vous voyez ci-dessous comment raccorder ces points d’entrée pour les implémentations de commandes. Pour plus d’informations sur les commandes, consultez Étendre les menus et les commandes.

Implémenter la classe ColumnGuideAdornment

La ColumnGuideAdornment classe est instanciée pour chaque vue de texte qui peut avoir des ornements. Cette classe écoute les événements relatifs à la modification ou aux paramètres de la vue et aux repères de colonnes de mise à jour ou de redessination si nécessaire.

Remplacez le contenu de ColumnGuideAdornment.cs par le code suivant (expliqué ci-dessous) :

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);
        }
    }

}

Les instances de cette classe tiennent sur les objets associés IWpfTextView et une liste d’objets Line dessinés sur la vue.

Le constructeur (appelé à partir du moment où ColumnGuideAdornmentTextViewCreationListener Visual Studio crée de nouvelles vues) crée les objets de repère Line de colonne. Le constructeur ajoute également des gestionnaires pour l’événement SettingsChanged (défini dans GuidesSettingsManager) et les événements LayoutChanged d’affichage et Closed.

L’événement LayoutChanged se déclenche en raison de plusieurs types de modifications dans la vue, notamment lorsque Visual Studio crée la vue. Le OnViewLayoutChanged gestionnaire appelle AddGuidelinesToAdornmentLayer à s’exécuter. Le code dans OnViewLayoutChanged détermine s’il doit mettre à jour les positions de ligne en fonction des modifications de taille de police, des coupes d’affichage, du défilement horizontal, etc. Le code dans provoque UpdatePositions le dessin des lignes de repère entre les caractères ou juste après la colonne de texte qui se trouve dans le décalage de caractères spécifié dans la ligne de texte.

Chaque fois que les paramètres modifient la SettingsChanged fonction, il suffit de recréer tous les Line objets avec les nouveaux paramètres. Après avoir défini les positions de ligne, le code supprime tous les objets précédents Line de la ColumnGuideAdornment couche d’ornement et ajoute les nouveaux.

Définir les commandes, les menus et les emplacements de menu

Il peut y avoir beaucoup à déclarer des commandes et des menus, en plaçant des groupes de commandes ou de menus sur différents autres menus et en raccordant des gestionnaires de commandes. Cette procédure pas à pas met en évidence le fonctionnement des commandes dans cette extension, mais pour plus d’informations, consultez Étendre les menus et les commandes.

Présentation du code

L’extension Guides de colonne montre la déclaration d’un groupe de commandes qui appartiennent ensemble (ajouter une colonne, supprimer une colonne, modifier la couleur de ligne), puis placer ce groupe dans un sous-menu du menu contextuel de l’éditeur. L’extension Guides de colonne ajoute également les commandes au menu d’édition principal, mais les conserve invisibles, décrites comme un modèle commun ci-dessous.

Il existe trois parties à l’implémentation des commandes : ColumnGuideCommandsPackage.cs, ColumnGuideCommandsPackage.vsct et ColumnGuideCommands.cs. Le code généré par les modèles place une commande dans le menu Outils qui affiche une boîte de dialogue comme implémentation. Vous pouvez voir comment cela est implémenté dans les fichiers .vsct et ColumnGuideCommands.cs , car il est simple. Vous remplacez le code dans ces fichiers ci-dessous.

Le code du package contient des déclarations réutilisables requises pour Visual Studio afin de découvrir que l’extension offre des commandes et de trouver où placer les commandes. Lorsque le package initialise, il instancie la classe d’implémentation des commandes. Pour plus d’informations sur les packages relatifs aux commandes, consultez Étendre les menus et les commandes.

Modèle de commandes commun

Les commandes de l’extension Guides de colonne sont un exemple de modèle très courant dans Visual Studio. Vous placez les commandes associées dans un groupe et vous placez ce groupe dans un menu principal, souvent avec «<CommandFlag>CommandWellOnly</CommandFlag> » défini pour rendre la commande invisible. Le fait de placer des commandes dans les menus principaux (par exemple , Modifier) leur donne des noms agréables (tels que Edit.AddColumnGuide), qui sont utiles pour rechercher des commandes lors de la réa assignation de liaisons de clés dans options d’outils. Il est également utile d’obtenir la saisie semi-automatique lors de l’appel de commandes à partir de la fenêtre commande.

Vous ajoutez ensuite le groupe de commandes aux menus contextuels ou sous-menus dans lesquels vous attendez que l’utilisateur utilise les commandes. Visual Studio traite uniquement CommandWellOnly comme un indicateur d’invisibilité pour les menus principaux. Lorsque vous placez le même groupe de commandes dans un menu contextuel ou un sous-menu, les commandes sont visibles.

Dans le cadre du modèle commun, l’extension Guides de colonne crée un deuxième groupe qui contient un sous-menu unique. Le sous-menu contient à son tour le premier groupe avec les commandes de repère à quatre colonnes. Le deuxième groupe qui contient le sous-menu est la ressource réutilisable que vous placez sur différents menus contextuels, qui place un sous-menu sur ces menus contextuels.

Fichier .vsct

Le fichier .vsct déclare les commandes et leur emplacement, ainsi que les icônes, et ainsi de suite. Remplacez le contenu du fichier .vsct par le code suivant (expliqué ci-dessous) :

<?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. Pour que Visual Studio recherche vos gestionnaires de commandes et les appelle, vous devez vous assurer que le GUID du package déclaré dans le fichier ColumnGuideCommandsPackage.cs (généré à partir du modèle d’élément de projet) correspond au GUID de package déclaré dans le fichier .vsct (copié à partir de ci-dessus). Si vous réutilyez cet exemple de code, vous devez vous assurer que vous disposez d’un GUID différent afin de ne pas entrer en conflit avec toute autre personne qui a peut-être copié ce code.

Recherchez cette ligne dans ColumnGuideCommandsPackage.cs et copiez le GUID entre les guillemets :

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

Ensuite, collez le GUID dans le fichier .vsct pour que vous ayez la ligne suivante dans vos Symbols déclarations :

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

Les GUID du jeu de commandes et le fichier image bitmap doivent également être uniques pour vos extensions :

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

Toutefois, vous n’avez pas besoin de modifier les GUID de l’ensemble de commandes et de l’image bitmap dans cette procédure pas à pas pour que le code fonctionne. Le GUID du jeu de commandes doit correspondre à la déclaration dans le fichier ColumnGuideCommands.cs , mais vous remplacez également le contenu de ce fichier. Par conséquent, les GUID correspondent.

Les autres GUID du fichier .vsct identifient les menus préexistants auxquels les commandes de repère de colonne sont ajoutées, de sorte qu’elles ne changent jamais.

Sections de fichier. Le fichier .vsct comporte trois sections externes : les commandes, les placements et les symboles. La section commandes définit des groupes de commandes, des menus, des boutons ou des éléments de menu et des bitmaps pour les icônes. La section placements déclare où les groupes passent sur des menus ou des placements supplémentaires sur des menus préexistants. La section symboles déclare des identificateurs utilisés ailleurs dans le fichier .vsct, ce qui rend le code .vsct plus lisible que d’avoir des GUID et des nombres hexadécimaux partout.

Section Commandes, définitions de groupes. La section commandes définit d’abord les groupes de commandes. Les groupes de commandes sont des commandes que vous voyez dans les menus avec de légères lignes grises séparant les groupes. Un groupe peut également remplir un sous-menu entier, comme dans cet exemple, et vous ne voyez pas les lignes de séparation grises dans ce cas. Les fichiers .vsct déclarent deux groupes, GuidesMenuItemsGroup qui sont parentés (IDM_VS_MENU_EDITle menu d’édition principal) et GuidesContextMenuGroup qui sont parentés (IDM_VS_CTXT_CODEWINmenu contextuel de l’éditeur de code).

La deuxième déclaration de groupe a une 0x0600 priorité :

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

L’idée est de placer le sous-menu repères de colonnes à la fin de n’importe quel menu contextuel auquel vous ajoutez le groupe de sous-menus. Mais, vous ne devez pas supposer que vous connaissez le mieux et forcer le sous-menu à toujours être en dernier en utilisant une priorité de 0xFFFF. Vous devez expérimenter le nombre pour voir où se trouve votre sous-menu sur les menus contextuels où vous le placez. Dans ce cas, 0x0600 il est assez élevé pour le placer à la fin des menus aussi loin que vous pouvez le voir, mais il laisse la place à quelqu’un d’autre de concevoir leur extension pour être inférieure à l’extension des repères de colonne si cela est souhaitable.

Section Commandes, définition de menu. Ensuite, la section de commande définit le sous-menu GuidesSubMenu, parenté du GuidesContextMenuGroup. Il GuidesContextMenuGroup s’agit du groupe que vous ajoutez à tous les menus contextuels pertinents. Dans la section placements, le code place le groupe avec les commandes de repère à quatre colonnes dans ce sous-menu.

Section Commandes, définitions de boutons. La section commandes définit ensuite les éléments de menu ou les boutons qui sont les commandes de repère à quatre colonnes. CommandWellOnly, abordé ci-dessus, signifie que les commandes sont invisibles lorsqu’elles sont placées dans un menu principal. Deux des déclarations de bouton d’élément de menu (ajouter un repère et supprimer le repère) ont également un AllowParams indicateur :

<CommandFlag>AllowParams</CommandFlag>

Cet indicateur permet, ainsi que d’avoir des emplacements de menu principal, la commande pour recevoir des arguments lorsque Visual Studio appelle le gestionnaire de commandes. Si l’utilisateur exécute la commande à partir de la fenêtre de commande, l’argument est transmis au gestionnaire de commandes dans les arguments d’événement.

Sections de commande, définitions de bitmaps. Enfin, la section commandes déclare les bitmaps ou les icônes utilisées pour les commandes. Cette section est une déclaration simple qui identifie la ressource de projet et répertorie les index basés sur un seul index d’icônes utilisées. La section symboles du fichier .vsct déclare les valeurs des identificateurs utilisés comme index. Cette procédure pas à pas utilise la bande bitmap fournie avec le modèle d’élément de commande personnalisé ajouté au projet.

Section Placements. Une fois la section commandes terminée, il s’agit de la section placements. La première est l’emplacement où le code ajoute le premier groupe décrit ci-dessus qui contient les commandes de repère à quatre colonnes dans le sous-menu où les commandes apparaissent :

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

Tous les autres placements ajoutent le GuidesContextMenuGroup (qui contient le GuidesSubMenu) à d’autres menus contextuels de l’éditeur. Lorsque le code a déclaré le GuidesContextMenuGroup, il a été parenté du menu contextuel de l’éditeur de code. C’est pourquoi vous ne voyez pas de placement pour le menu contextuel de l’éditeur de code.

Section Symboles. Comme indiqué ci-dessus, la section symboles déclare les identificateurs utilisés ailleurs dans le fichier .vsct, ce qui rend le code .vsct plus lisible que d’avoir des GUID et des nombres hexadécimaux partout. Les points importants de cette section sont que le GUID du package doit accepter la déclaration dans la classe de package. Et le GUID du jeu de commandes doit accepter la déclaration dans la classe d’implémentation de commande.

Implémenter les commandes

Le fichier ColumnGuideCommands.cs implémente les commandes et connecte les gestionnaires. Lorsque Visual Studio charge le package et l’initialise, le package appelle Initialize à son tour la classe d’implémentation des commandes. L’initialisation des commandes instancie simplement la classe, et le constructeur connecte tous les gestionnaires de commandes.

Remplacez le contenu du fichier ColumnGuideCommands.cs par le code suivant (expliqué ci-dessous) :

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 the 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);
                }
            }
        }

    }
}

Corrigez les références. Vous ne disposez pas d’une référence à ce stade. Appuyez sur le bouton pointeur droit sur le nœud Références dans le Explorateur de solutions. Choisissez la commande Ajouter ... . La boîte de dialogue Ajouter une référence comporte une zone de recherche dans le coin supérieur droit. Entrez « editor » (sans guillemets doubles). Choisissez l’élément Microsoft.VisualStudio.Editor (vous devez case activée la zone à gauche de l’élément, pas seulement sélectionner l’élément) et choisir OK pour ajouter la référence.

Initialisation. Lorsque la classe de package initialise, elle appelle Initialize la classe d’implémentation des commandes. L’initialisation ColumnGuideCommands instancie la classe et enregistre l’instance de classe et la référence de package dans les membres de classe.

Examinons l’un des raccordements du gestionnaire de commandes à partir du constructeur de classe :

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

Vous créez un OleMenuCommand. Visual Studio utilise le système de commandes Microsoft Bureau. Les arguments clés lors de l’instanciation d’un OleMenuCommand est la fonction qui implémente la commande (AddColumnGuideExecuted), la fonction à appeler lorsque Visual Studio affiche un menu avec la commande (AddColumnGuideBeforeQueryStatus) et l’ID de commande. Visual Studio appelle la fonction d’état de requête avant d’afficher une commande dans un menu afin que la commande puisse se rendre invisible ou grisée pour un affichage particulier du menu (par exemple, désactiver la copie s’il n’y a pas de sélection), modifier son icône ou même modifier son nom (par exemple, à partir d’Ajouter quelque chose à supprimer), et ainsi de suite. L’ID de commande doit correspondre à un ID de commande déclaré dans le fichier .vsct . Les chaînes du jeu de commandes et des repères de colonnes ajoutent une commande doivent correspondre entre le fichier .vsct et columnGuideCommands.cs.

La ligne suivante fournit de l’aide lorsque les utilisateurs appellent la commande via la fenêtre commande (expliquée ci-dessous) :

_addGuidelineCommand.ParametersDescription = "<column>";

État de la requête. Les fonctions AddColumnGuideBeforeQueryStatus d’état de la requête et RemoveColumnGuideBeforeQueryStatus case activée certains paramètres (tels que le nombre maximal de repères ou la colonne maximale) ou s’il existe un repère de colonne à supprimer. Ils activent les commandes si les conditions sont appropriées. Les fonctions d’état des requêtes doivent être efficaces, car elles s’exécutent chaque fois que Visual Studio affiche un menu et pour chaque commande du menu.

AddColumnGuideExecuted, fonction. La partie intéressante de l’ajout d’un guide consiste à déterminer l’emplacement actuel de l’éditeur et de l’emplacement d’insertion. Tout d’abord, cette fonction appelle GetApplicableColumn, qui case activée s’il existe un argument fourni par l’utilisateur dans les arguments d’événement du gestionnaire de commandes, et s’il n’y en a aucun, la fonction case activée la vue de l’éditeur :

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 doit creuser un peu pour obtenir une IWpfTextView vue du code. Si vous suivez GetActiveTextView, GetActiveViewet GetTextViewFromVsTextView, vous pouvez voir comment procéder. Le code suivant est le code approprié extrait, en commençant par la sélection actuelle, puis en obtenant l’image de la sélection, puis en obtenant le DocView de l’image en tant que IVsTextViewdocument, puis en obtenant une IVsUserData à partir de IVsTextView, puis en obtenant un hôte d’affichage et enfin l’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;

Une fois que vous disposez d’un IWpfTextView, vous pouvez obtenir la colonne où se trouve le point d’insertion :

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));
}

Avec la colonne active dans laquelle l’utilisateur a cliqué, le code appelle simplement le gestionnaire de paramètres pour ajouter ou supprimer la colonne. Le gestionnaire de paramètres déclenche l’événement auquel tous les ColumnGuideAdornment objets écoutent. Lorsque l’événement se déclenche, ces objets mettent à jour leurs vues de texte associées avec de nouveaux paramètres de repère de colonne.

Appeler la commande à partir de la fenêtre commande

L’exemple de repères de colonnes permet aux utilisateurs d’appeler deux commandes à partir de la fenêtre de commandes sous la forme d’extensibilité. Si vous utilisez la vue | Autres fenêtres | Commande De la fenêtre de commande, vous pouvez voir la fenêtre commande. Vous pouvez interagir avec la fenêtre de commande en entrant « edit ». Avec la saisie semi-automatique du nom de la commande et en fournissant l’argument 120, vous disposez du résultat suivant :

> Edit.AddColumnGuide 120
>

Les éléments de l’exemple qui activent ce comportement se trouvent dans les déclarations de fichier .vsct, le ColumnGuideCommands constructeur de classe lorsqu’il connecte des gestionnaires de commandes et les implémentations de gestionnaires de commandes qui case activée arguments d’événement.

Vous avez vu «<CommandFlag>CommandWellOnly</CommandFlag> » dans le fichier .vsct , ainsi que les placements dans le menu principal Modifier , même si les commandes ne sont pas affichées dans l’interface utilisateur du menu Modifier . Les avoir dans le menu Édition principal leur donne des noms tels que Edit.AddColumnGuide. Déclaration de groupe de commandes qui contient les quatre commandes placées directement dans le menu Modifier :

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

La section boutons a ultérieurement déclaré les commandes pour les garder invisibles CommandWellOnly dans le menu principal et les a déclarées avec 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>

Vous avez vu le code de raccordement du gestionnaire de commandes dans le ColumnGuideCommands constructeur de classe fourni une description du paramètre autorisé :

_addGuidelineCommand.ParametersDescription = "<column>";

Vous avez vu la GetApplicableColumn fonction case activée s OleMenuCmdEventArgs pour une valeur avant de case activée l’affichage de l’éditeur pour une colonne actuelle :

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;
    }

Essayer votre extension

Vous pouvez maintenant appuyer sur F5 pour exécuter votre extension Guides de colonne. Ouvrez un fichier texte et utilisez le menu contextuel de l’éditeur pour ajouter des lignes de repère, les supprimer et modifier leur couleur. Cliquez dans le texte (pas d’espace blanc passé la fin de la ligne) pour ajouter un repère de colonne, ou l’éditeur l’ajoute à la dernière colonne de la ligne. Si vous utilisez la fenêtre commande et appelez les commandes avec un argument, vous pouvez ajouter des repères de colonnes n’importe où.

Si vous souhaitez essayer différents placements de commandes, modifier les noms, modifier les icônes, et ainsi de suite, et que vous rencontrez des problèmes avec Visual Studio montrant le dernier code dans les menus, vous pouvez réinitialiser la ruche expérimentale dans laquelle vous déboguez. Affichez le menu Démarrer de Windows et tapez « réinitialiser ». Recherchez et exécutez la commande, réinitialisez l’instance expérimentale Visual Studio suivante. Cette commande propre la ruche de Registre expérimental de tous les composants d’extension. Il n’propre pas les paramètres des composants. Par conséquent, les guides que vous aviez lorsque vous arrêtez la ruche expérimentale de Visual Studio sont toujours là lorsque votre code lit le magasin de paramètres lors du prochain lancement.

Projet de code terminé

Il y aura bientôt un projet GitHub d’exemples d’extensibilité Visual Studio, et le projet terminé sera là. Cet article sera mis à jour pour pointer là-bas quand cela se produit. L’exemple de projet terminé peut avoir des guid différents et aura une bande de bitmaps différente pour les icônes de commande.

Vous pouvez essayer une version de la fonctionnalité guides de colonnes avec cette extension de galerieVisual Studio.