Freigeben über


Exemplarische Vorgehensweise: Erstellen einer Randsteuerelement-Ansicht, von Befehlen und Einstellungen (Spaltenmarkierungen)

Sie können den Text-/Code-Editor von Visual Studio um Befehle und Ansichtseffekte erweitern. Dieser Artikel zeigt Ihnen, wie Sie mit einer beliebten Erweiterungsfunktion, den Spaltenmarkierungen, beginnen können. Spaltenmarkierungen sind visuelle, helle Linien, die in der Ansicht des Texteditors dargestellt werden und Ihnen helfen, Ihren Code in bestimmten Spaltenbreiten zu verwalten. Formatierter Code kann insbesondere für Beispiele wichtig sein, die Sie in Dokumente, Blogbeiträge oder Fehlerberichte einbinden.

In dieser exemplarischen Vorgehensweise:

  • Erstellen eines VSIX-Projekts

  • Hinzufügen eines Randsteuerelements für die Editoransicht

  • Hinzufügen von Unterstützung für das Speichern und Abrufen von Einstellungen (wo Spaltenmarkierungen in welcher Farbe dargestellt werden sollen)

  • Befehle hinzufügen (Spaltenmarkierungen hinzufügen/entfernen, ihre Farbe ändern)

  • Platzieren von Befehlen im Menü Bearbeiten und in den Kontextmenüs von Textdokumenten

  • Hinzufügen der Unterstützung für das Aufrufen der Befehle aus dem Visual Studio-Befehlsfenster

    Sie können eine Version der Spaltenmarkierungen-Funktion mit dieser Visual Studio Gallery Erweiterung ausprobieren.

    Hinweis

    In dieser exemplarischen Vorgehensweise fügen Sie eine große Menge Code in ein paar Dateien ein, die von Visual Studio-Erweiterungsvorlagen generiert werden. In Kürze wird diese exemplarische Vorgehensweise jedoch auf eine fertige Lösung auf GitHub mit anderen Erweiterungsbeispielen verweisen. Der vollständige Code unterscheidet sich insofern leicht, als er echte Befehlssymbole enthält, anstatt Symbole aus der generischen Vorlage zu verwenden.

Die Lösung einrichten

Zunächst erstellen Sie ein VSIX-Projekt, fügen ein Randsteuerelement für den Editor hinzu und fügen dann einen Befehl hinzu (wodurch ein VSPackage erstellt wird, um den Befehl zu verwalten). Die grundlegende Architektur ist wie folgt:

  • Sie haben einen Listener für die Erstellung einer Textansicht, der ein ColumnGuideAdornment-Objekt pro Ansicht erstellt. Dieses Objekt wartet auf Ereignisse, bei denen sich die Ansicht oder die Einstellungen ändern, und aktualisiert gegebenenfalls die Spaltenmarkierungen oder zeichnet sie neu.

  • Es gibt einen GuidesSettingsManager, der das Lesen und Schreiben aus dem Storage für Visual Studio-Einstellungen übernimmt. Der Einstellungsmanager verfügt auch über Vorgänge zum Aktualisieren der Einstellungen, die die Benutzer*innen unterstützen (Spalte hinzufügen, Spalte entfernen, Farbe ändern).

  • Es gibt ein VSIP-Paket, das notwendig ist, wenn Sie Benutzerbefehle haben, aber es handelt sich dabei nur um Code, der das Implementierungsobjekt für die Befehle initialisiert.

  • Es gibt ein ColumnGuideCommands-Objekt, das die Benutzerbefehle ausführt und die Handler für die in der VSCT-Datei deklarierten Befehle einbindet.

    VSIX. Verwenden Sie den Befehl Datei | Neu ..., um ein Projekt zu erstellen. Wählen Sie den Knoten Erweiterbarkeit unter C# im linken Navigationsbereich und wählen Sie VSIX Projekt im rechten Fensterbereich. Geben Sie den Namen ColumnGuides ein und wählen Sie OK, um das Projekt zu erstellen.

    Randsteuerelement anzeigen. Wählen Sie im Lösungs-Explorer die rechte Schaltfläche des Projektknotens. Wählen Sie den Befehl Hinzufügen | Neues Element ..., um ein neues Randsteuerelement hinzuzufügen. Wählen Sie Erweiterung | Editor im linken Navigationsbereich und wählen Sie Editor Viewport-Randsteuerelement im rechten Bereich. Geben Sie den Namen ColumnGuideAdornment als Elementname ein und wählen Sie Hinzufügen, um es hinzuzufügen.

    Sie sehen, dass diese Elementvorlage dem Projekt zwei Dateien (sowie Referenzen usw.) hinzugefügt hat: ColumnGuideAdornment.cs und ColumnGuideAdornmentTextViewCreationListener.cs. Die Vorlagen stellen ein lila Rechteck auf der Ansicht dar. Im folgenden Abschnitt ändern Sie ein paar Zeilen im Listener für die Ansichtserstellung und ersetzen den Inhalt von ColumnGuideAdornment.cs.

    Befehle. Wählen Sie im Lösungs-Explorer die rechte Schaltfläche des Projektknotens. Wählen Sie den Befehl Hinzufügen | Neues Element ..., um ein neues Randsteuerelement hinzuzufügen. Wählen Sie Erweiterbarkeit | VSPackage im linken Navigationsbereich und wählen Sie Angepasster Befehl im rechten Bereich. Geben Sie den Namen ColumnGuideCommands als Elementnamen ein und wählen Sie Hinzufügen. Neben mehreren Referenzen wurden durch das Hinzufügen der Befehle und des Pakets auch ColumnGuideCommands.cs, ColumnGuideCommandsPackage.cs und ColumnGuideCommandsPackage.vsct hinzugefügt. Im folgenden Abschnitt ersetzen Sie den Inhalt der ersten und letzten Datei, um die Befehle zu definieren und zu implementieren.

Einrichten des Listeners für die Erstellung der Textansicht

Öffnen Sie ColumnGuideAdornmentTextViewCreationListener.cs im Editor. Dieser Code implementiert einen Handler für den Fall, dass Visual Studio Textansichten erstellt. Es gibt Attribute, die steuern, wann der Handler in Abhängigkeit von den Eigenschaften der Ansicht aufgerufen wird.

Der Code muss auch eine Randsteuerelement-Schicht deklarieren. Wenn der Editor Ansichten aktualisiert, ruft er die Randsteuerelement-Schichten aus den Schichten der Ansicht ab. Sie können die Anordnung Ihrer Schicht relativ zu anderen mit Attributen angeben. Ersetzen Sie die folgende Zeile:

[Order(After = PredefinedAdornmentLayers.Caret)]

mit diesen beiden Zeilen:

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

Die Zeile, die Sie ersetzt haben, gehört zu einer Gruppe von Attributen, die eine Randsteuerelement-Schicht deklarieren. Die erste Zeile, die Sie geändert haben, ändert nur, wo die Spaltenmarkierungen erscheinen. Wenn Sie die Linien in der Ansicht „vor“ dem Text darstellen, erscheinen sie hinter oder unter dem Text. Die zweite Zeile erklärt, dass die Spaltenmarkierungs-Randsteuerelemente auf Entitäten anwendbar sind, die Ihrer Vorstellung von einem Dokument entsprechen, aber Sie könnten die Randsteuerelemente zum Beispiel so deklarieren, dass sie nur für bearbeitbaren Text funktioniert. Weitere Informationen finden Sie unter Sprachdienst und Editor-Erweiterungspunkte

Implementieren des Einstellungsmanagers

Ersetzen Sie den Inhalt der Datei GuidesSettingsManager.cs durch den folgenden Code (siehe unten):

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;

    }
}

Der größte Teil dieses Codes erstellt und parst das Einstellungsformat: „RGB(<int>,<int>,<int>) <int>, <int>, ...“. Die Ganzzahlen am Ende sind die einwertigen Spalten, für die Sie Spaltenmarkierungen wünschen. Die Spaltenmarkierungs-Erweiterung fasst alle Einstellungen in einer einzigen Zeichenfolge für Einstellungswerte zusammen.

Es gibt einige Teile des Codes, die hervorzuheben sind. Die folgende Zeile des Codes ruft den von Visual Studio verwalteten Wrapper für den Einstellungsmanager-Speicher auf. Dieser abstrahiert größtenteils von der Registrierung von Windows, aber diese API ist unabhängig vom Storage-Mechanismus.

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

Der Visual Studio-Einstellungsspeicher verwendet einen Kategoriebezeichner und einen Einstellungsbezeichner, um alle Einstellungen eindeutig zu identifizieren:

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

Sie müssen nicht "Text Editor" als Kategorienamen verwenden. Sie können einen beliebigen Namen wählen.

Die ersten paar Funktionen sind die Einstiegspunkte zum Ändern von Einstellungen. Sie überprüfen übergeordnete Einschränkungen wie die maximal zulässige Anzahl von Markierungen. Dann rufen sie WriteSettings auf, das eine Zeichenfolge für die Einstellungen kombiniert und die Eigenschaft GuideLinesConfiguration festlegt. Das Festlegen dieser Eigenschaft speichert den Einstellungswert im Speicher von Visual Studio und löst das Ereignis SettingsChanged aus, um alle ColumnGuideAdornment-Objekte zu aktualisieren, die jeweils mit einer Textansicht verbunden sind.

Es gibt eine Reihe von Einstiegspunkt-Funktionen, wie CanAddGuideline, die zur Implementierung von Befehlen verwendet werden, die Einstellungen ändern. Wenn Visual Studio Menüs anzeigt, fragt es die Implementierungen der Befehle ab, um zu sehen, ob der Befehl gerade aktiviert ist, wie er heißt usw. Im Folgenden sehen Sie, wie Sie diese Einstiegspunkte für die Befehlsimplementierungen einbinden. Weitere Informationen über Befehle finden Sie unter Erweiterung von Menüs und Befehlen.

Implementierung der Klasse ColumnGuideAdornment

Die ColumnGuideAdornment Klasse wird für jede Textansicht instanziiert, die Randsteuerelemente haben kann. Diese Klasse wartet auf Ereignisse, bei denen sich die Ansicht oder die Einstellungen ändern, und auf die Aktualisierung oder Neuzeichnung von Spaltenmarkierungen, falls erforderlich.

Ersetzen Sie den Inhalt von ColumnGuideAdornment.cs durch den folgenden Code (unten erklärt):

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

}

Instanzen dieser Klasse halten das zugehörige IWpfTextView und eine Liste von Line-Objekten fest, die auf der Ansicht dargestellt werden.

Der Konstruktor (der von ColumnGuideAdornmentTextViewCreationListener aufgerufen wird, wenn Visual Studio neue Ansichten erstellt) erstellt die Line-Objekte der Spaltenmarkierung. Der Konstruktor fügt auch Handler für das Ereignis SettingsChanged (definiert in GuidesSettingsManager) und die Ansichtsereignisse LayoutChanged und Closed hinzu.

Das Ereignis LayoutChanged wird durch verschiedene Arten von Änderungen in der Ansicht ausgelöst, unter anderem wenn Visual Studio die Ansicht erstellt. Der Handler OnViewLayoutChanged ruft AddGuidelinesToAdornmentLayer zur Ausführung auf. Der Code in OnViewLayoutChanged bestimmt, ob er die Zeilenpositionen aufgrund von Änderungen wie Schriftgrößenänderungen, Linien in der Ansicht, horizontalem Scrollen und so weiter aktualisieren muss. Der Code in UpdatePositions bewirkt, dass Hilfslinien zwischen den Zeichen oder direkt nach der Textspalte dargestellt werden, die sich im angegebenen Zeichenoffset in der Textzeile befindet.

Wenn sich die Einstellungen ändern, erstellt die SettingsChanged-Funktion einfach alle Line-Objekte mit den neuen Einstellungen neu. Nach dem Festlegen der Zeilenpositionen entfernt der Code alle vorherigen Line-Objekte aus der ColumnGuideAdornment-Randsteuerelement-Schicht und fügt die neuen Objekte hinzu.

Definieren der Befehle, Menüs und Menüplatzierungen

Es kann eine Menge Aufwand bedeuten, Befehle und Menüs zu deklarieren, Gruppen von Befehlen oder Menüs auf verschiedenen anderen Menüs zu platzieren und Befehlshandler einzubinden. Diese exemplarische Vorgehensweise zeigt Ihnen, wie die Befehle in dieser Erweiterung funktionieren. Weitere Informationen finden Sie unter Erweiterung von Menüs und Befehlen.

Einführung in den Code

Die Spaltenmarkierungs-Erweiterung zeigt, wie Sie eine Gruppe von zusammengehörenden Befehlen deklarieren (Spalte hinzufügen, Spalte entfernen, Zeilenfarbe ändern) und diese Gruppe dann in einem Untermenü des Kontextmenüs des Editors platzieren. Die Spaltenmarkierungs-Erweiterung fügt die Befehle auch in das Hauptmenü Bearbeiten ein, lässt sie aber unsichtbar, was weiter unten als allgemeines Muster strukturiert wird.

Die Implementierung der Befehle besteht aus drei Teilen: ColumnGuideCommandsPackage.cs, ColumnGuideCommandsPackage.vsct, und ColumnGuideCommands.cs. Der von den Vorlagen generierte Code fügt dem Menü Tools einen Befehl hinzu, der als Implementierung ein Dialogfeld öffnet. Sie können sich die Implementierung in den Dateien .vsct und ColumnGuideCommands.cs ansehen, da sie sehr einfach ist. Sie ersetzen den Code in den Dateien unten.

Der Code des Pakets enthält Boilerplate-Deklarationen, die Visual Studio benötigt, um zu erkennen, dass die Erweiterung Befehle anbietet und um herauszufinden, wo die Befehle zu platzieren sind. Wenn das Paket initialisiert wird, instanziiert es die Klasse für die Implementierung von Befehlen. Weitere Informationen über Pakete im Zusammenhang mit Befehlen finden Sie unter Erweiterung von Menüs und Befehlen.

Ein allgemeines Befehle-Muster

Die Befehle in der Spaltenmarkierungs-Erweiterung sind ein Beispiel für ein sehr allgemeines Muster in Visual Studio. Sie fassen zusammengehörige Befehle in einer Gruppe zusammen und platzieren diese Gruppe in einem Hauptmenü, oft mit der Einstellung "<CommandFlag>CommandWellOnly</CommandFlag>", um den Befehl unsichtbar zu machen. Wenn Sie die Befehle in die Hauptmenüs aufnehmen (z. B. Bearbeiten), geben Sie ihnen nützliche Namen (z. B. Edit.AddColumnGuide), die Ihnen helfen, die Befehle zu finden, wenn Sie die Tastenkombinationen in Tools Optionen neu zuweisen. Das ist auch hilfreich für die Vervollständigung, wenn Sie Befehle aus dem Befehlsfenster aufrufen.

Anschließend fügen Sie die Befehlsgruppe den Kontextmenüs oder Untermenüs hinzu, in denen Sie erwarten, dass Benutzer*innen die Befehle verwenden. Visual Studio berücksichtigt CommandWellOnly nur als Flag für die Unsichtbarkeit von Hauptmenüs. Wenn Sie dieselbe Gruppe von Befehlen in einem Kontextmenü oder Untermenü platzieren, sind die Befehle sichtbar.

Als Teil des allgemeinen Musters erstellt die Spaltenmarkierungs-Erweiterung eine zweite Gruppe, die ein einzelnes Untermenü enthält. Das Untermenü enthält wiederum die erste Gruppe mit den vierspaltigen Befehlen. Die zweite Gruppe, die das Untermenü enthält, ist das wiederverwendbare Asset, das Sie in verschiedenen Kontextmenüs platzieren, wodurch ein Untermenü in diesen Kontextmenüs erscheint.

Die .vsct-Datei

In der Datei .vsct werden die Befehle und ihre Position sowie die Symbole usw. festgelegt. Ersetzen Sie den Inhalt der Datei .vsct durch den folgenden Code (siehe unten):

<?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>

GUIDS. Damit Visual Studio Ihre Handler finden und aufrufen kann, müssen Sie sicherstellen, dass die in der Datei ColumnGuideCommandsPackage.cs (die aus der Vorlage für das Projektelement generiert wurde) deklarierte Paket-GUID mit der Paket-GUID übereinstimmt, die in der Datei .vsct (von oben kopiert) deklariert wurde. Wenn Sie diesen Beispielcode wiederverwenden, sollten Sie sicherstellen, dass Sie eine andere GUID verwenden, damit Sie nicht mit anderen Personen in Konflikt geraten, die diesen Code möglicherweise kopiert haben.

Suchen Sie diese Zeile in ColumnGuideCommandsPackage.cs und kopieren Sie die GUID zwischen den Anführungszeichen:

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

Fügen Sie dann die GUID in die Datei .vsct ein, sodass Sie die folgende Zeile in Ihren Symbols-Deklarationen haben:

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

Die GUIDs für den Befehlssatz und die Bitmap-Bilddatei sollten für Ihre Erweiterungen ebenfalls eindeutig sein:

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

Sie müssen die GUIDs für den Befehlssatz und das Bitmap-Bild in dieser exemplarischen Vorgehensweise jedoch nicht ändern, damit der Code funktioniert. Die GUID für den Befehlssatz muss mit der Deklaration in der Datei ColumnGuideCommands.cs übereinstimmen, aber Sie ersetzen auch den Inhalt dieser Datei; daher werden die GUIDs übereinstimmen.

Andere GUIDs in der Datei .vsct kennzeichnen bereits vorhandene Menüs, zu denen die Anleitungen für die Spaltenmarkierungen hinzugefügt werden, so dass sie sich nie ändern.

Dateiabschnitte. Die .vsct hat drei äußere Abschnitte: Commands, Placements und Symbols. Im Abschnitt commands werden Befehlsgruppen, Menüs, Schaltflächen oder Menüpunkte und Bitmaps für Symbole definiert. Der Abschnitt placements legt fest, wo Gruppen in Menüs oder zusätzliche Platzierungen in bereits bestehenden Menüs erscheinen. Im Abschnitt symbols werden Bezeichner deklariert, die an anderer Stelle in der Datei .vsct verwendet werden. Dadurch wird der .vsct-Code besser lesbar, als wenn überall GUIDs und Hexadezimalzahlen stehen.

Abschnitt commands, Gruppendefinitionen. Im Abschnitt commands werden zunächst Befehlsgruppen definiert. Befehlsgruppen sind Befehle, die Sie in Menüs mit leichten grauen Linien zwischen den Gruppen sehen. Eine Gruppe kann auch ein ganzes Untermenü ausfüllen, wie in diesem Beispiel, und in diesem Fall sehen Sie die grauen Trennlinien nicht. Die .vsct-Dateien deklarieren zwei Gruppen, die GuidesMenuItemsGroup, die der IDM_VS_MENU_EDIT (dem Hauptmenü Bearbeiten) übergeordnet ist und die GuidesContextMenuGroup, die der IDM_VS_CTXT_CODEWIN (dem Kontextmenü des Code-Editors) übergeordnet ist.

Die zweite Gruppendeklaration hat eine 0x0600-Priorität:

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

Die Idee ist, das Untermenü Spaltenmarkierungen an das Ende eines jeden Kontextmenüs zu setzen, dem Sie die Untermenügruppe hinzufügen. Aber Sie sollten nicht davon ausgehen, dass Sie es am besten wissen und das Untermenü mit einer Priorität von 0xFFFF immer an letzter Stelle platzieren. Sie müssen mit der Zahl experimentieren, um zu sehen, wo Ihr Untermenü in den Kontextmenüs liegt, in denen Sie es platzieren. In diesem Fall ist 0x0600 hoch genug, um es am Ende der Menüs zu platzieren, soweit Sie das sehen können, aber es lässt Raum für jemand anderen, seine Erweiterung niedriger als die Spaltenmarkierungs-Erweiterung zu gestalten, wenn das erwünscht ist.

Abschnitt commands, Menüdefinition. Der Abschnitt commands definiert das Untermenü GuidesSubMenu, das dem Menü GuidesContextMenuGroup übergeordnet ist. GuidesContextMenuGroup ist die Gruppe, die Sie zu allen relevanten Kontextmenüs hinzufügen. Im Abschnitt placements platziert der Code die Gruppe mit den vierspaltigen Befehlen in diesem Untermenü.

Abschnitt commands, Definitionen der Schaltflächen. Im Abschnitt commands werden dann die Menüpunkte oder Schaltflächen definiert, die vierspaltigen Befehle darstellen. CommandWellOnly, wie oben beschrieben, bedeutet, dass die Befehle unsichtbar sind, wenn sie in einem Hauptmenü platziert werden. Zwei der Deklarationen der Schaltflächen für die Menüeinträge (Spalte hinzufügen und Spalte entfernen) haben außerdem ein AllowParams-Flag:

<CommandFlag>AllowParams</CommandFlag>

Dieses Flag sorgt dafür, dass der Befehl nicht nur im Hauptmenü platziert wird, sondern auch Argumente erhält, wenn Visual Studio den Handler für den Befehl aufruft. Wenn der Benutzer*innen den Befehl vom Befehlsfenster aus ausführen, wird das Argument in den Ereignisargumenten an den Befehls-Handler übergeben.

Befehlsabschnitte, Bitmaps-Definitionen. Schließlich deklariert der Abschnitt commands die Bitmaps oder Symbole, die für die Befehle verwendet werden. Bei diesem Abschnitt handelt es sich um eine einfache Deklaration, die die Projektressource identifiziert und einseitige Indizes der verwendeten Symbole auflistet. Der Abschnitt symbols der Datei .vsct deklariert die Werte der Bezeichner, die als Indizes verwendet werden. In dieser exemplarischen Vorgehensweise wird der Bitmap-Streifen verwendet, der mit der dem Projekt hinzugefügten angepassten Vorlage für Befehlselemente bereitgestellt wird.

Abschnitt Placements. Nach dem Abschnitt commands kommt der Abschnitt placements. Im ersten Abschnitt fügt der Code die oben beschriebene erste Gruppe hinzu, die die vierspaltigen Befehle für das Untermenü enthält, in dem die Befehle erscheinen:

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

Alle anderen Platzierungen fügen das GuidesContextMenuGroup (das das GuidesSubMenu enthält) zu anderen Kontextmenüs des Editors hinzu. Als der Code das GuidesContextMenuGroup deklarierte, wurde es dem Kontextmenü des Code-Editors übergeordnet. Deshalb sehen Sie auch keine Platzierung für das Kontextmenü des Code-Editors.

Symbole. Wie bereits erwähnt, werden im Abschnitt symbols Bezeichner deklariert, die an anderer Stelle in der Datei .vsct verwendet werden. Dadurch wird der Code in .vsct besser lesbar, als wenn überall GUIDs und Hexadezimalzahlen stehen. Die wichtigsten Punkte in diesem Abschnitt sind, dass die Paket-GUID mit der Deklaration in der Paketklasse übereinstimmen muss. Und die GUID des Befehlssatzes muss mit der Deklaration in der Klasse für die Implementierung des Befehls übereinstimmen.

Implementierung der Befehle

Die Datei ColumnGuideCommands.cs implementiert die Befehle und bindet die Handler ein. Wenn Visual Studio das Paket lädt und initialisiert, ruft das Paket seinerseits Initialize für die Implementierungsklasse der Befehle auf. Bei der Initialisierung von commands wird die Klasse einfach instanziiert, und der Konstruktor bindet alle Handler für die Befehle ein.

Ersetzen Sie den Inhalt der Datei ColumnGuideCommands.cs durch den folgenden Code (siehe unten):

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

    }
}

Referenzen reparieren. Sie vermissen an dieser Stelle eine Referenz. Wählen Sie im Lösungs-Explorer die rechte Schaltfläche des Knotens Referenzen. Wählen Sie den Befehl Hinzufügen .... Das Dialogfeld Referenz hinzufügen hat in der oberen rechten Ecke ein Suchfeld. Geben Sie „editor“ (ohne die Anführungszeichen) ein. Wählen Sie den Eintrag Microsoft.VisualStudio.Editor (Sie müssen das Kästchen links neben dem Eintrag markieren, nicht nur den Eintrag auswählen) und wählen Sie OK, um die Referenz hinzuzufügen.

Initialisierung. Wenn die Paketklasse initialisiert wird, ruft sie Initialize für die Klasse zur Implementierung von Befehlen auf. Die ColumnGuideCommands-Initialisierung instanziiert die Klasse und speichert die Instanz der Klasse und den Verweis auf das Paket in den Klassenmitgliedern.

Schauen wir uns eine der Verknüpfungen des Befehls-Handlers mit dem Klassenkonstruktor an:

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

Sie erstellen ein OleMenuCommand. Visual Studio verwendet das Befehlssystem von Microsoft Office. Die wichtigsten Argumente bei der Instanziierung eines OleMenuCommand sind die Funktion, die den Befehl implementiert (AddColumnGuideExecuted), die Funktion, die aufgerufen werden soll, wenn Visual Studio ein Menü mit dem Befehl anzeigt (AddColumnGuideBeforeQueryStatus), und die Befehls-ID. Visual Studio ruft die Funktion zur Abfrage des Status auf, bevor es einen Befehl in einem Menü anzeigt, sodass der Befehl für einen bestimmten Bildschirm des Menüs unsichtbar oder ausgegraut werden kann (z. B. Kopieren deaktivieren, wenn es keine Auswahl gibt), sein Symbol oder sogar seinen Namen ändern kann (z. B. von Etwas hinzufügen zu Etwas entfernen), usw. Die Befehls-ID muss mit einer in der Datei .vsct angegebenen Befehls-ID übereinstimmen. Die Zeichenfolgen für den Befehlssatz und den Befehl Spaltenmarkierungen hinzufügen müssen zwischen der Datei .vsct und der Datei ColumnGuideCommands.cs übereinstimmen.

Die folgende Zeile dient als Hilfestellung, wenn Benutzer*innen den Befehl über das Befehlsfenster aufrufen (siehe unten):

_addGuidelineCommand.ParametersDescription = "<column>";

Abfragestatus. Die Statusabfragefunktionen AddColumnGuideBeforeQueryStatus und RemoveColumnGuideBeforeQueryStatus überprüfen einige Einstellungen (z. B. maximale Anzahl der Linien oder maximale Spalte) oder ob eine Spaltenmarkierung zu entfernen ist. Sie aktivieren die Befehle, wenn die Bedingungen erfüllt sind. Die Statusabfrage-Funktionen müssen effizient sein, da sie jedes Mal ausgeführt werden, wenn Visual Studio ein Menü anzeigt und für jeden Befehl im Menü.

FunctionAddColumnGuideExecuted. Der interessante Teil des Hinzufügens einer Spalte besteht darin, die aktuelle Editoransicht und die Position der Einfügemarke herauszufinden. Zunächst ruft diese Funktion GetApplicableColumn auf, die prüft, ob ein vom Benutzer*innen eingegebenes Argument in den Ereignisargumenten des Befehls-Handlers vorhanden ist, und wenn nicht, prüft die Funktion die Ansicht des Editors:

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 muss ein wenig nachforschen, um einen IWpfTextView-Blick auf den Code zu werfen. Wenn Sie den Weg durch GetActiveTextView, GetActiveView und GetTextViewFromVsTextView verfolgen, können Sie sehen, wie das geht. Der folgende Code ist der relevante Code in abstrahierter Form. Er beginnt mit der aktuellen Auswahl, ruft dann das Frame der Auswahl ab, holt sich dann die DocView des Frames als IVsTextView, holt sich dann ein IVsUserData von IVsTextView, holt sich dann einen View Host und schließlich die 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;

Sobald Sie eine IWpfTextView haben, können Sie die Spalte ermitteln, in der sich die Einfügemarke befindet:

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

Mit der aktuellen Spalte, auf die der Benutzer geklickt hat, ruft der Code einfach den Einstellungsmanager auf, um die Spalte hinzuzufügen oder zu entfernen. Der Einstellungsmanager löst das Ereignis aus, auf das alle ColumnGuideAdornment-Objekte reagieren. Wenn das Ereignis ausgelöst wird, aktualisieren diese Objekte ihre zugehörigen Textansichten mit den neuen Einstellungen für die Spaltenmarkierung.

Aufrufen des Befehls vom Befehlsfenster aus

Das Beispiel für Spaltenmarkierungen ermöglicht es den Benutzer*innen, zwei Befehle aus dem Befehlsfenster als Erweiterungsform aufzurufen. Wenn Sie den Befehl Ansicht | Andere Windows | Befehlsfenster verwenden, können Sie das Befehlsfenster sehen. Sie können mit dem Befehlsfenster interagieren, indem Sie „edit.“ eingeben. Mit der Vervollständigung des Befehlsnamens und der Angabe des Arguments 120 erhalten Sie das folgende Ergebnis:

> Edit.AddColumnGuide 120
>

Die Teile des Beispiels, die dieses Verhalten ermöglichen, befinden sich in den Deklarationen der .vsct-Datei, im ColumnGuideCommands-Klassenkonstruktor, wenn er Befehlshandler einbindet, und in den Implementierungen der Befehlshandler, die Ereignisargumente überprüfen.

Sie haben „<CommandFlag>CommandWellOnly</CommandFlag>“ in der .vsct-Datei sowie die Platzierung im Bearbeiten-Hauptmenü gesehen, obwohl die Befehle nicht in der Bearbeiten-Menüoberfläche angezeigt werden. Da sie sich im Hauptmenü Bearbeiten befinden, erhalten sie Namen wie Edit.AddColumnGuide. Die Deklaration der Befehlsgruppe, die die vier Befehle enthält, platziert die Gruppe direkt im Menü Bearbeiten:

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

Im Abschnitt buttons wurden die Befehle später mit CommandWellOnly deklariert, um sie im Hauptmenü unsichtbar zu machen, und mit AllowParams deklariert:

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

Sie haben gesehen, dass der Code des Handlers für die Befehle im Konstruktor der Klasse ColumnGuideCommands eine Beschreibung der zulässigen Parameter bietet:

_addGuidelineCommand.ParametersDescription = "<column>";

Sie haben gesehen, dass die Funktion GetApplicableColumn OleMenuCmdEventArgs auf einen Wert prüft, bevor sie die Ansicht des Editors auf eine aktuelle Spalte überprüft:

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

Testen Ihrer Erweiterung

Sie können nun F5 drücken, um Ihre Spaltenmarkierungs-Erweiterung auszuführen. Öffnen Sie eine Textdatei und verwenden Sie das Kontextmenü des Editors, um Linien hinzuzufügen, sie zu entfernen und ihre Farbe zu ändern. Klicken Sie in den Text (nicht in das Leerzeichen am Ende der Zeile), um eine Spaltenmarkierung hinzuzufügen, oder der Editor fügt sie in der letzten Spalte der Zeile ein. Wenn Sie das Befehlsfenster verwenden und die Befehle mit einem Argument aufrufen, können Sie Spaltenmarkierungen an beliebiger Stelle einfügen.

Wenn Sie verschiedene Befehlsplatzierungen ausprobieren, Namen ändern, Symbole ändern usw. und Probleme damit haben, dass Visual Studio Ihnen den neuesten Code in den Menüs anzeigt, können Sie die experimentelle Hive, in der Sie debuggen, zurücksetzen. Rufen Sie das Windows-Startmenü auf und geben Sie „reset“ ein. Suchen Sie den Befehl Nächste experimentelle Instanz von Visual Studio zurücksetzen und führen Sie ihn aus. Dieser Befehl bereinigt die experimentelle Registrierungsstruktur von allen Komponenten der Erweiterung. Er löscht nicht die Einstellungen der Komponenten, so dass alle Spalten, die Sie beim Beenden der experimentellen Instanz von Visual Studio hatten, immer noch vorhanden sind, wenn Ihr Code beim nächsten Start den Speicher für die Einstellungen liest.

Fertiges Code-Projekt

Es wird bald ein GitHub-Projekt mit Visual Studio-Extensibility-Beispielen geben, und das fertige Projekt wird dort zu finden sein. Dieser Artikel wird aktualisiert, um auf dieses Projekt zu verweisen, wenn es soweit ist. Das fertige Beispielprojekt wird möglicherweise andere Linien und einen anderen Bitmap-Streifen für die Befehlssymbole haben.

Sie können eine Version der Spaltenmarkierungen-Funktion mit dieser Visual Studio Gallery Erweiterung ausprobieren.