Freigeben über


Exemplarische Vorgehensweise: Erstellen von Randsteuerelementen für eine Ansicht, Befehlen und Einstellungen (Satzspiegel)

Sie können den Visual Studio-Text-/Code-Editor mit Befehlen und Ansichtseffekten erweitern. In diesem Artikel erfahren Sie, wie Sie mit einer beliebten Erweiterungsfunktion, Spaltenführungen, beginnen. Spaltenführungen sind visuell helle Linien, die in der Ansicht des Text-Editors gezeichnet werden, um dir zu helfen, deinen Code für bestimmte Spaltenbreiten zu formatieren. Insbesondere formatierter Code kann für Beispiele wichtig sein, die Sie in Dokumente, Blogbeiträge oder Fehlerberichte einschließen.

In dieser exemplarischen Vorgehensweise führen Sie folgende Aktionen aus:

  • Erstellen eines VSIX-Projekts

  • Hinzufügen einer Dekoration für die Editoransicht

  • Hinzufügen der Unterstützung für das Speichern und Abrufen von Einstellungen in Bezug auf die Position und Farbe von Satzspiegeln

  • Befehle hinzufügen (Spaltenführungslinien hinzufügen/entfernen, farbe ändern)

  • Platzieren der Befehle im Menü "Bearbeiten" und Kontextmenüs für Textdokumenten

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

    Sie können eine Version des Features für Satzspiegel mit dieser Visual Studio Gallery-Erweiterung ausprobieren.

    Anmerkung

    In dieser exemplarischen Vorgehensweise fügen Sie einen großen Code in einige Dateien ein, die von Visual Studio-Erweiterungsvorlagen generiert wurden. In Kürze wird diese exemplarische Vorgehensweise jedoch auf eine fertige Lösung auf GitHub mit anderen Erweiterungsbeispielen verweisen. Der fertige Code unterscheidet sich geringfügig davon, dass er echte Befehlssymbole enthält, anstatt generische Vorlagensymbole zu verwenden.

Einrichten der Lösung

Zunächst erstellen Sie ein VSIX-Projekt und fügen eine Randsteuerelement für die Editor-Ansicht hinzu. Anschließend fügen Sie einen Befehl hinzu, der ein VSPackage-Element hinzufügt, das den Befehl besitzen soll. Die grundlegende Architektur lautet wie folgt:

  • Sie verfügen über einen Listener für die Erstellung der Textansicht, der ein ColumnGuideAdornment-Objekt pro Ansicht erstellt. Dieses Objekt lauscht auf Ereignisse in Bezug auf Änderungen an der Ansicht oder den Einstellungen und aktualisiert oder zeichnet Spaltenführungslinien bei Bedarf neu.

  • Das GuidesSettingsManager-Element verwaltet Lese- und Schreibvorgänge aus dem Visual Studio-Einstellungsspeicher. Der Einstellungs-Manager verfügt auch über Vorgänge zum Aktualisieren der Einstellungen, die die Benutzerbefehle unterstützen (Spalte hinzufügen, Spalte entfernen, Farbe ändern).

  • Es gibt ein VSIP-Paket, das erforderlich ist, wenn Sie über Benutzerbefehle verfügen, aber es handelt sich lediglich um Codebausteine, der das Implementierungsobjekt der Befehle initialisiert.

  • Es gibt ein ColumnGuideCommands-Objekt, das die Benutzerbefehle ausführt und die Befehlshandler für Befehle bindet, die in der .vsct--Datei deklariert sind.

    VSIX: Verwenden Sie den Befehl Datei | Neu ... zum Erstellen eines Projekts. Wählen Sie im linken Navigationsbereich unter C# den Knoten Erweiterbarkeit aus, und wählen Sie im rechten Bereich VSIX-Projekt aus. Geben Sie den Namen ColumnGuides- ein, und wählen Sie OK aus, um das Projekt zu erstellen.

    Randsteuerelement für eine Ansicht: Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Projektknoten. Wählen Sie den Befehl Hinzufügen | Neues Element ..., um ein neues Ansichtszierelement hinzuzufügen. Wählen Sie im linken Navigationsbereich Erweiterbarkeit | Editor und im rechten Bereich Randsteuerelement für den Editor-Viewport aus. Geben Sie den Namen ColumnGuideAdornment als Elementnamen ein, und wählen Sie Hinzufügen aus, um das Element hinzuzufügen.

    Sie können sehen, dass diese Elementvorlage dem Projekt zwei Dateien hinzugefügt hat (sowie Verweise usw.): ColumnGuideAdornment.cs und ColumnGuideAdornmentTextViewCreationListener.cs. Die Vorlagen zeichnen in der Ansicht ein lila Rechteck. Im folgenden Abschnitt ändern Sie einige Zeilen im Listener für die Erstellung von Ansichten und ersetzen den Inhalt von ColumnGuideAdornment.cs.

    Befehle: Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Projektknoten. Wählen Sie den Befehl Hinzufügen | Neues Element ..., um ein neues Ansichtszierelement hinzuzufügen. Wählen Sie im linken Navigationsbereich Erweiterbarkeit | VSPackage und im rechten Bereich Benutzerdefinierter Befehl aus. Geben Sie den Namen ColumnGuideCommands als Elementnamen ein, und wählen Sie Hinzufügen aus. Zusätzlich zu mehreren Verweisen wurden die Befehle und das Paket außerdem den Dateien ColumnGuideCommands.cs, ColumnGuideCommandsPackage.cs und ColumnGuideCommandsPackage.vsct hinzugefügt. Im folgenden Abschnitt ersetzen Sie den Inhalt der ersten und letzten Dateien, um die Befehle zu definieren und zu implementieren.

Richten Sie den Listener für die Erstellung der Textansicht ein

Öffnen Sie ColumnGuideAdornmentTextViewCreationListener.cs im Editor. Dieser Code implementiert einen Handler für jedes Mal, wenn Visual Studio Textansichten erstellt. Es gibt Attribute, die steuern, wann der Handler je nach Merkmalen der Ansicht aufgerufen wird.

Der Code muss auch eine Zierschicht deklarieren. Wenn der Editor Ansichten aktualisiert, ruft er die Verzierungsebenen für die Ansicht ab und ruft daraus die Zierelemente ab. Sie können die Reihenfolge Ihrer Ebene relativ zu anderen mit Attributen deklarieren. Ersetzen Sie die folgende Zeile:

[Order(After = PredefinedAdornmentLayers.Caret)]

Ersetzen Sie sie durch diese beiden Zeilen:

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

Die ersetzte Linie befindet sich in einer Gruppe von Attributen, die eine Verzierungsschicht deklarieren. Die erste Zeile, die Sie geändert haben, ändert sich nur, wo die Spaltenführungslinien angezeigt werden. Das Zeichnen der Zeilen "vor" dem Text in der Ansicht bedeutet, dass sie hinter oder unterhalb des Texts angezeigt werden. In der zweiten Zeile wird deklariert, dass die Randsteuerelemente für den Satzspiegel für Textentitäten gelten, die Ihrem Konzept eines Dokuments entsprechen. Sie könnten die Deklaration jedoch so formulieren, dass der Satzspiegel ausschließlich bei bearbeitbarem Text funktioniert. Weitere Informationen finden Sie in Sprachdienst- und Editorerweiterungspunkten

Implementieren des Einstellungs-Managers

Ersetzen Sie den Inhalt der 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 großteil dieses Codes erstellt und analysiert das Einstellungsformat: "RGB(<int>,<int>,<int>) <int>, <int>, ...". Bei den ganzen Zahlen am Ende handelt es sich um die Spalten auf Basis von 1, in denen Satzspiegel angezeigt werden sollen. Die Satzspiegelerweiterung erfasst alle Einstellungen in einer einzelnen Einstellungswertzeichenfolge.

Es gibt einige Teile des Codes, der hervorgehoben werden sollte. Die folgende Codezeile ruft den von Visual Studio verwalteten Wrapper für den Einstellungsspeicher ab. Dabei handelt es sich größtenteils um eine Abstrahierung über die Windows-Registrierung, diese API ist jedoch vom Speichermechanismus unabhängig.

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

Der Speicher für Visual Studio-Einstellungen 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 "Text Editor" nicht als Kategorienamen verwenden. Sie können alles auswählen, was Ihnen gefällt.

Die ersten Funktionen sind die Einstiegspunkte zum Ändern von Einstellungen. Sie überprüfen übergeordnete Einschränkungen wie die maximale Anzahl zulässiger Führungen. Anschließend rufen sie WriteSettingsauf, wodurch eine Einstellungszeichenfolge erstellt und die Eigenschaft GuideLinesConfigurationfestgelegt wird. Durch Festlegen dieser Eigenschaft wird der Einstellungswert im Visual Studio-Einstellungsspeicher gespeichert und das SettingsChanged Ereignis ausgelöst, um alle ColumnGuideAdornment Objekte zu aktualisieren, die jeweils einer Textansicht zugeordnet sind.

Es gibt einige Einstiegspunktfunktionen, z. B. CanAddGuideline, die zum Implementieren von Befehlen verwendet werden, die Einstellungen ändern. Wenn Visual Studio Menüs anzeigt, werden Befehlsimplementierungen abgefragt, um festzustellen, ob der Befehl derzeit aktiviert ist, was der Name ist usw. Im Folgenden erfahren Sie, wie Sie diese Einstiegspunkte für die Befehlsimplementierungen verbinden. Weitere Informationen zu Befehlen finden Sie unter Erweitern von Menüs und Befehlen.

Implementieren der ColumnGuideAdornment-Klasse

Die ColumnGuideAdornment-Klasse wird für jede Textansicht instanziiert, die Randsteuerelemente enthalten kann. Diese Klasse lauscht auf Ereignisse zum Ändern der Ansicht oder Einstellungen und gegebenenfalls zum Aktualisierung oder erneuten Zeichnen von Satzspiegeln.

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

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 die zugeordnete IWpfTextView-Schnittstelle und eine Liste der Line-Objekte, die in der Ansicht gezeichnet wurden.

Der Konstruktor (aufgerufen durch ColumnGuideAdornmentTextViewCreationListener, wenn Visual Studio neue Ansichten erstellt) erstellt die Objekte der Spaltenführung Line. Der Konstruktor fügt außerdem Handler für das SettingsChanged-Ereignis (definiert in GuidesSettingsManager) und die Ansichtsereignisse LayoutChanged und Closedhinzu.

Das LayoutChanged-Ereignis wird durch verschiedene Änderungen in der Ansicht ausgelöst, unter anderem, wenn Visual Studio die Ansicht erstellt. Der OnViewLayoutChanged-Handler ruft AddGuidelinesToAdornmentLayer zum Ausführen auf. Der Code in OnViewLayoutChanged bestimmt, ob Zeilenpositionen basierend auf Änderungen wie unter anderem Änderungen des Schriftgrads, Ansichtsbundstege, horizontales Scrollen aktualisiert werden müssen. Durch den Code in UpdatePositions werden Führungslinien zwischen Zeichen oder direkt nach der Textspalte gezeichnet, die sich in der Textzeile im angegebenen Zeichenoffset befindet.

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

Definieren der Befehle, Menüs und Menüplatzierung

Es gibt viel zu beachten, wenn es darum geht, Befehle und Menüs zu deklarieren, Gruppen von Befehlen oder Menüs in verschiedenen anderen Menüs zu platzieren und Befehlshandler zu verbinden. In dieser exemplarischen Vorgehensweise wird erläutert, wie Befehle in dieser Erweiterung funktionieren, aber ausführlichere Informationen finden Sie unter Erweitern von Menüs und Befehlen.

Einführung in den Code

Die Erweiterung "Spaltenführungslinien" zeigt das Deklarieren einer Gruppe von Befehlen, die zusammen gehören (Spalte hinzufügen, Spalte entfernen, Linienfarbe ändern) und diese Gruppe dann in einem Untermenü des Kontextmenüs des Editors zu platzieren. Die Satzspiegelerweiterung fügt dem Hauptmenü Bearbeiten außerdem die Befehle hinzu. Diese sind jedoch nicht sichtbar und werden unten als allgemeines Muster behandelt.

Es gibt drei Teile der Befehlsimplementierung: ColumnGuideCommandsPackage.cs, ColumnGuideCommandsPackage.vsct und ColumnGuideCommands.cs. Der von den Vorlagen generierte Code fügt einen Befehl im Menü Tools ein, in dem ein Dialogfeld als Implementierung angezeigt wird. Sie können sich ansehen, wie dies in der .vsct und ColumnGuideCommands.cs Dateien implementiert wird, da es einfach ist. Sie ersetzen den Code in diesen Dateien unten.

Der Paketcode enthält Deklarationen, die für Visual Studio erforderlich sind, um zu ermitteln, dass die Erweiterung Befehle bietet, und um zu ermitteln, wo die Befehle platziert werden sollen. Wenn das Paket initialisiert wird, instanziiert es die Implementierungsklasse der Befehle. Weitere Informationen zu Paketen im Zusammenhang mit Befehlen finden Sie unter Erweitern von Menüs und Befehlen.

Ein gängiges Befehlsmuster

Die Befehle in der Erweiterung "Spaltenführungslinien" sind ein Beispiel für ein sehr gängiges Muster in Visual Studio. Sie platzieren verwandte Befehle in einer Gruppe, und sie platzieren diese Gruppe in einem Hauptmenü, häufig mit "<CommandFlag>CommandWellOnly</CommandFlag>" festgelegt, um den Befehl unsichtbar zu machen. Das Platzieren von Befehlen in den Hauptmenüs (z. B. Bearbeiten) bietet ihnen schöne Namen (z. B. Edit.AddColumnGuide), die hilfreich für die Suche nach Befehlen beim erneuten Zuweisen von Tastenbindungen in Tools Optionssind. Sie ist ebenfalls für die Vervollständigung beim Aufrufen von Befehle aus dem Befehlsfenster hilfreich.

Anschließend fügen Sie die Gruppe von Befehlen zu Kontextmenüs oder Untermenüs hinzu, in denen der Benutzer die Befehle verwenden soll. Visual Studio behandelt CommandWellOnly als Unsichtbarkeitskennzeichen nur für 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 Satzspiegelerweiterung eine zweite Gruppe, die ein einzelnes Untermenü enthält. Das Untermenü enthält wiederum die erste Gruppe mit den vier Satzspiegelbefehlen. Die zweite Gruppe, die das Untermenü enthält, ist die wiederverwendbare Ressource, die Sie in verschiedenen Kontextmenüs platzieren, wodurch ein Untermenü in diese Kontextmenüs eingefügt wird.

Die VSCT-Datei

Die .vsct-Datei deklariert die Befehle und deren Position, zusammen mit Symbolen usw. 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 Die Befehlshandler finden und diese aufrufen kann, müssen Sie sicherstellen, dass die in der ColumnGuideCommandsPackage.cs-Datei deklarierte Paket-GUID (generiert aus der Projektelementvorlage) mit der paket-GUID übereinstimmt, die in der .vsct--Datei deklariert ist (kopiert von oben). Wenn Sie diesen Beispielcode erneut verwenden, sollten Sie sicherstellen, dass Sie über eine andere GUID verfügen, sodass Sie keinen Konflikt mit anderen Personen haben, die diesen Code kopiert haben.

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

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

Fügen Sie dann die GUID in die .vsct-Datei 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 Bitmapbilddatei sollten auch für Ihre Erweiterungen eindeutig sein:

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

In dieser exemplarischen Vorgehensweise müssen Sie die Befehlssatz- und Bitmapbild-GUIDs jedoch nicht ändern, um den Code funktionsfähig zu machen. Die GUID des Befehlssatzes muss mit der Deklaration in der Datei ColumnGuideCommands.cs übereinstimmen. Da Sie jedoch den Inhalt dieser Datei ebenfalls ersetzen, stimmen die GUIDs überein.

Andere GUIDs in der .vsct Datei identifizieren bereits vorhandene Menüs, denen die Befehle für die Spaltenführung hinzugefügt werden, sodass sie sich nie ändern.

Abschnitte für Dateien: Die VSCT-Datei weist drei äußere Abschnitte auf: Befehle, Platzierungen und Symbole. Der Abschnitt "Befehle" definiert Befehlsgruppen, Menüs, Schaltflächen oder Menüelemente sowie Bitmaps für Symbole. Im Abschnitt für Platzierungen wird deklariert, wo Gruppen in Menüs oder zusätzliche Platzierungen in bereits vorhandenen Menüs platziert werden. Im Abschnitt "Symbole" werden Bezeichner deklariert, die an anderer Stelle in der .vsct--Datei verwendet werden, wodurch der .vsct Code besser lesbar ist als GUIDs und Hexadenznummern überall.

Befehlsabschnitt, Gruppendefinitionen. Der Abschnitt "Befehle" definiert zuerst Befehlsgruppen. Gruppen von Befehlen sind Befehle, die in Menüs mit leichten grauen Linien angezeigt werden, die die Gruppen trennen. Eine Gruppe kann auch ein gesamtes Untermenü ausfüllen, wie in diesem Beispiel, und in diesem Fall werden die grauen Trennlinien nicht angezeigt. Die .vsct--Dateien deklarieren zwei Gruppen, die GuidesMenuItemsGroup, die der IDM_VS_MENU_EDIT zugeordnet ist (das Hauptmenü Bearbeiten) und die GuidesContextMenuGroup, die dem IDM_VS_CTXT_CODEWIN zugeordnet ist (kontextmenü des Code-Editors).

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

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

Dadurch wird das Untermenü für Satzspiegel am Ende des Kontextmenüs platziert, dem Sie die Untermenügruppe hinzufügen. Aber Sie sollten nicht davon ausgehen, dass Sie es besser wissen und erzwingen, dass das Untermenü immer zuletzt mit einer Priorität von 0xFFFFerscheint. Sie müssen mit der Zahl experimentieren, um zu sehen, wo sich Ihr Untermenü in den Kontextmenüs befindet, wo Sie es platzieren. In diesem Fall ist 0x0600 hoch genug, um es am Ende der Menüs soweit es sichtbar ist zu platzieren, aber es lässt Raum, damit jemand seine Erweiterung so gestalten kann, dass sie niedriger als die Erweiterung der Spaltenführungen ist, sofern dies gewünscht ist.

Befehlsabschnitt, Menüdefinition. Als Nächstes definiert der Befehlsabschnitt das Untermenü GuidesSubMenu, das der GuidesContextMenuGroupzugeordnet ist. Die GuidesContextMenuGroup ist die Gruppe, die Sie allen relevanten Kontextmenüs hinzufügen. Durch den Code im Abschnitt für Platzierungen wird die Gruppe mit den vier Satzspiegelbefehlen in diesem Untermenü platziert.

Befehlsabschnitt, Schaltflächendefinitionen. Der Befehlsabschnitt definiert dann die Menüelemente oder Schaltflächen, die die Vierspaltenführungsbefehle sind. CommandWellOnly, oben erläutert, bedeutet, dass die Befehle unsichtbar sind, wenn sie in einem Hauptmenü platziert werden. Zwei der Deklarationen der Menüelementschaltfläche („Führungslinie hinzufügen“ und „Führungslinie entfernen“) weisen außerdem ein AllowParams-Flag auf:

<CommandFlag>AllowParams</CommandFlag>

Dieses Flag ermöglicht zusammen mit der Platzierungen im Hauptmenü den Befehl zum Empfangen von Argumenten, wenn der Befehlshandler von Visual Studio aufgerufen wird. Wenn der Benutzer den Befehl über das Befehlsfenster ausführt, wird das Argument an den Befehlshandler in den Ereignisargumenten übergeben.

Befehlsabschnitte, Bitmapdefinitionen. Schließlich deklariert der Abschnitt "Befehle" die Bitmaps oder Symbole, die für die Befehle verwendet werden. Dieser Abschnitt ist eine einfache Deklaration, die die Projektressource identifiziert und Indizes verwendeter Symbole auflistet. Im Abschnitt "Symbole" der .vsct-Datei werden die Werte der als Indizes verwendeten Bezeichner deklariert. In dieser exemplarischen Vorgehensweise wird die Bitmapleiste verwendet, die mit der dem Projekt hinzugefügten Vorlage für Elemente benutzerdefinierter Befehle bereitgestellt wird.

Abschnitt für Platzierungen: Nach dem Befehlsabschnitt folgt der Abschnitt "Platzierungen". Im ersten Abschnitt wird die oben beschriebene erste Gruppe hinzugefügt, in der die vier Satzspiegelbefehle enthalten sind, die im Untermenü angezeigt werden:

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

Alle anderen Platzierungen fügen die GuidesContextMenuGroup (die die GuidesSubMenuenthält) in die Kontextmenüs anderer Editoren hinzu. Durch die Deklarierung des GuidesContextMenuGroup-Elements im Code wurde es dem Kontextmenü des Code-Editors übergeordnet. Aus diesem Grund wird keine Platzierung für das Kontextmenü des Code-Editors angezeigt.

Abschnitt für Symbole: Wie oben erwähnt, deklariert der Abschnitt "Symbole" Bezeichner, die an anderer Stelle in der .vsct--Datei verwendet werden, wodurch der .vsct Code besser lesbar ist als GUIDs und Hexadenzahlen überall. Die wichtigen Punkte in diesem Abschnitt sind, dass die Paket-GUID der Deklaration in der Paketklasse zustimmen muss. Außerdem muss die GUID des Befehlssatzes mit der Deklaration in der Befehlsimplementierungsklasse übereinstimmen.

Implementieren der Befehle

Die ColumnGuideCommands.cs Datei implementiert die Befehle und bindet die Handler an. Wenn Visual Studio das Paket lädt und initialisiert, ruft das Paket wiederum Initialize für die Implementierungsklasse der Befehle auf. Die Initialisierung der Befehle instanziiert einfach die Klasse, und der Konstruktor bindet alle Befehlshandler an.

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

    }
}

Korrigieren der Verweisen: An diesem Punkt fehlt ein Verweis. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Knoten „References“. Wählen Sie den Befehl Hinzufügen ... aus. Das Dialogfeld Verweis hinzufügen enthält ein Suchfeld in der oberen rechten Ecke. Geben Sie "editor" ein (ohne doppelte Anführungszeichen). Wählen Sie das Microsoft.VisualStudio.Editor Element aus (Sie müssen das Kontrollkästchen links neben dem Element aktivieren, nicht nur das Element auswählen), und wählen Sie OK aus, um den Verweis hinzuzufügen.

Initialisierung: Beim Initialisieren der Paketklasse wird Initialize für die Implementierungsklasse der Befehle aufgerufen. Die ColumnGuideCommands-Initialisierung instanziiert die Klasse und speichert die Klasseninstanz sowie den Paketverweis in den Mitgliedern der Klasse.

Sehen wir uns eine der Befehlshandler-Verknüpfungen aus dem Klassenkonstruktor an.

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

Sie erstellen eine OleMenuCommand-Klasse. Visual Studio verwendet das Microsoft Office-Befehlssystem. Die Schlüsselargumente beim Instanziieren einer OleMenuCommand sind die Funktion, die den Befehl implementiert (AddColumnGuideExecuted), die Funktion, die aufgerufen werden soll, wenn Visual Studio ein Menü mit dem Befehl (AddColumnGuideBeforeQueryStatus) und der Befehls-ID anzeigt. Visual Studio ruft die Statusabfragefunktion auf, bevor ein Befehl in einem Menü angezeigt wird, sodass der Befehl für eine bestimmte Anzeige des Menüs unsichtbar oder ausgegraut erscheint (z. B. wenn Kopieren deaktiviert wird, wenn keine Auswahl vorhanden ist), sein Symbol ändern oder sogar seinen Namen ändern kann (z. B. von 'Etwas hinzufügen' in 'Etwas entfernen'), und so weiter. Die Befehls-ID muss mit einer Befehls-ID übereinstimmen, die in der datei .vsct deklariert ist. Die Zeichenfolgen für den Befehlssatz und die Befehle zum Hinzufügen der Satzspiegel müssen in der VSCT-Datei und der Datei ColumnGuideCommands.cs übereinstimmen.

Die folgende Zeile bietet Unterstützung, wenn Benutzer den Befehl über das Befehlsfenster aufrufen (siehe unten):

_addGuidelineCommand.ParametersDescription = "<column>";

Status der Abfrage. Die Abfragestatusfunktionen AddColumnGuideBeforeQueryStatus und RemoveColumnGuideBeforeQueryStatus überprüfen einige Einstellungen (z. B. die maximale Anzahl von Leitlinien oder die maximale Spaltenzahl) oder ob eine Spaltenführungslinie entfernt werden soll. Sie aktivieren die Befehle, wenn die Bedingungen richtig sind. Abfragestatusfunktionen müssen effizient sein, da sie jedes Mal ausgeführt werden, wenn Visual Studio ein Menü und für jeden Befehl im Menü anzeigt.

AddColumnGuideExecuted-Funktion: Beim Hinzufügen eines Spaltenspiegels ist das Ermitteln der aktuellen Editor-Ansicht und der Position des Textcursor besonders interessant. Zunächst ruft diese Funktion GetApplicableColumnauf, wodurch überprüft wird, ob ein vom Benutzer bereitgestelltes Argument in den Ereignisargumenten des Befehlshandlers vorhanden ist, und wenn keines vorhanden ist, überprü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 graben, um eine IWpfTextView Ansicht des Codes zu erhalten. Wenn Sie GetActiveTextView, GetActiveViewund GetTextViewFromVsTextViewnachverfolgen, können Sie sehen, wie das geht. Bei dem folgenden Code handelt es sich um die Abstraktion des relevanten Codes. Diese beginnt mit der aktuellen Auswahl und ruft nacheinander die Auswahl des Frames, das DocView-Element des Frames als eine IVsTextView-Schnittstelle, eine IVsUserData-Schnittstelle aus der IVsTextView-Schnittstelle, einen Ansichtshost und schließlich die IWpfTextView-Schnittstelle ab:

   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 über eine IWpfTextView-Schnittstelle verfügen, können Sie die Spalte abrufen, in der sich der Textcursor 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 Benutzende geklickt hat, weist der Code den Einstellungs-Manager lediglich an, die Spalte hinzuzufügen oder zu entfernen. Der Einstellungs-Manager löst das Ereignis aus, auf das alle ColumnGuideAdornment-Objekte lauschen. Wenn das Ereignis ausgelöst wird, aktualisieren diese Objekte ihre zugeordneten Textansichten mit neuen Einstellungen für den Satzspiegel.

Aufrufen des Befehls aus dem Befehlsfenster

Das Beispiel für Satzspiegel ermöglicht Benutzenden das Aufrufen von zwei Befehlen aus dem Befehlsfenster als Form der Erweiterbarkeit. Wenn Sie den Befehl Ansicht | Weitere Fenster | Befehlsfenster verwenden, wird das Befehlsfenster angezeigt. Sie können mit dem Befehlsfenster interagieren, indem Sie "edit." eingeben, und mit der Vervollständigung des Befehlsnamens sowie durch das Angeben des Arguments 120 haben Sie das folgende Ergebnis:

> Edit.AddColumnGuide 120
>

Die Teile des Beispiels, das dieses Verhalten aktivieren, befinden sich in den .vsct Dateideklarationen, dem ColumnGuideCommands Klassenkonstruktor, wenn Befehlshandler eingebunden werden, und die Befehlshandlerimplementierungen, die Ereignisargumente überprüfen.

Das <CommandFlag>CommandWellOnly</CommandFlag>-Element befindet sich zwar in der VSCT-Datei und in den Platzierungen im Hauptmenü Bearbeiten, die Befehle werden jedoch nicht auf der Benutzeroberfläche des Menüs Bearbeiten angezeigt. Durch die Platzierung dieser Befehle im Hauptmenü Bearbeiten erhalten sie Namen wie Edit.AddColumnGuide. Die Befehlsgruppendeklaration, 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>

Der Abschnitt Schaltflächen hat später die Befehle CommandWellOnly deklariert, um sie im Hauptmenü unsichtbar zu halten, und sie mit AllowParamsdeklariert.

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

In diesem Artikel wurde veranschaulicht, wie der Verknüfpungscode des Befehlshandlers im ColumnGuideCommands-Klassenkonstruktor eine Beschreibung des zulässigen Parameters bereitstellt:

_addGuidelineCommand.ParametersDescription = "<column>";

Sie haben gesehen, dass die GetApplicableColumn-Funktion OleMenuCmdEventArgs auf einen Wert überprüft, bevor in der Ansicht des Editors nach einer aktuellen Spalte gesucht wird.

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

Probieren Sie Ihre Erweiterung aus

Sie können jetzt F5 drücken, um die Satzspiegelerweiterung auszuführen. Öffnen Sie eine Textdatei, und verwenden Sie das Kontextmenü des Editors, um Führungslinien hinzuzufügen, sie zu entfernen und ihre Farbe zu ändern. Klicken Sie auf den Text und nicht auf das Leerzeichen am Ende der Zeile, um einen Satzspiegel hinzuzufügen. Andernfalls fügt der Editor diesen der letzten Spalte in der Zeile hinzu. Wenn Sie das Befehlsfenster verwenden und die Befehle mit einem Argument aufrufen, können Sie überall Spaltenführungslinien hinzufügen.

Wenn Sie unterschiedliche Befehlsplatzierungen ausprobieren, Namen ändern, Symbole ändern usw. und wenn Sie Probleme mit der Anzeige des neuesten Codes in Menüs durch Visual Studio haben, können Sie die Experimentiereinstellung zurücksetzen, in der Sie debuggen. Rufen Sie das Windows-Startmenü auf, und geben Sie "Zurücksetzen" ein. Suchen Sie den Befehl Nächste Visual Studio Experimental-Instanz zurücksetzen, und führen Sie ihn aus. Durch diesen Befehl wird die experimentelle Registrierungsstruktur aller Erweiterungskomponenten bereinigt. Es werden keine Einstellungen aus Komponenten entfernt. Daher sind alle Satzspiegel immer noch vorhanden, die beim Beenden der experimentellen Struktur von Visual Studio vorhanden waren, wenn Ihr Code den Einstellungsspeicher beim nächsten Start liest.

Fertiges Codeprojekt

Es wird bald ein GitHub-Projekt von Visual Studio Extensibility-Beispielen geben, und das fertige Projekt wird dort sein. Dieser Artikel wird aktualisiert, um darauf hinzuweisen, wann dies geschieht. Das fertige Beispielprojekt verfügt möglicherweise über unterschiedliche GUIDs und verfügt über einen anderen Bitmapsstreifen für die Befehlssymbole.

Sie können eine Version des Features für Satzspiegel mit dieser Visual Studio Gallery-Erweiterung ausprobieren.