Procédure-à-pas : Créer un ornement de vue, des commandes et des paramètres (guides de colonne)
Vous pouvez étendre l'éditeur de texte/code de Visual Studio avec des commandes et des effets de vue. Cet article vous montre comment commencer avec une fonctionnalité d'extension populaire, les guides de colonne. Les guides de colonne sont des lignes visuellement légères tracées sur la vue de l'éditeur de texte pour vous aider à gérer votre code selon des largeurs de colonne spécifiques. En particulier, le code formaté peut être important pour les échantillons que vous incluez dans des documents, des articles de blog ou des rapports de bogues.
Lors de cette procédure pas à pas, vous allez effectuer les opérations suivantes :
Créer un projet VSIX
Ajouter un ornement à la vue de l'éditeur
Ajout d'un support pour la sauvegarde et l'obtention de paramètres (où dessiner les guides de colonne et leur couleur)
Ajoutez des commandes (ajouter/supprimer des guides de colonne, changer leur couleur).
Placez les commandes dans le menu Edition et les menus contextuels du document texte
Ajouter la possibilité d'invoquer les commandes à partir de la fenêtre de commande de Visual Studio.
Vous pouvez tester une version de la fonctionnalité de guides de colonnes avec cette extension de la galerie Visual Studio.
Remarque
Dans cette procédure pas-à-pas, vous collez une grande quantité de code dans quelques fichiers générés par les modèles d'extension de Visual Studio. Mais, bientôt, cette procédure-à-pas-pas fera référence à une solution complète sur GitHub avec d'autres exemples d'extensions. Le code complété est légèrement différent en ce sens qu'il a de vraies icônes de commande au lieu d'utiliser les icônes du modèle générique.
Configurer la solution
Tout d'abord, vous créez un projet VSIX, ajoutez un ornement de vue d'éditeur, puis ajoutez une commande (qui ajoute un VSPackage pour posséder la commande). L'architecture de base est la suivante :
Vous avez un listener de création de vue de texte qui crée un objet
ColumnGuideAdornment
par vue. Cet objet est à l'écoute des événements concernant le changement de vue ou de paramètres, mettant à jour ou redessinant les guides de colonne si nécessaire.Il y a un
GuidesSettingsManager
qui gère la lecture et l'écriture à partir de la mémoire des paramètres de Visual Studio. Le gestionnaire de paramètres a également des opérations pour mettre à jour les paramètres qui supportent les commandes de l'utilisateur (ajouter une colonne, supprimer une colonne, changer la couleur).Un package VSIP est nécessaire si vous avez des commandes utilisateur, mais il s'agit simplement d'un code de base qui initialise l'objet d'implémentation des commandes.
Il y a un objet
ColumnGuideCommands
qui exécute les commandes de l'utilisateur et accroche les gestionnaires des commandes déclarées dans le fichier .vsct.VSIX. Utilisez la commande Fichier | Nouveau ... pour créer un projet. Choisissez le nœud Extensibility sous C# dans le volet de navigation de gauche et choisissez VSIX Project dans le volet de droite. Saisissez le nom ColumnGuides et cliquez sur OK pour créer le projet.
Voir l'ornement. Appuyez sur le bouton droit de la souris sur le nœud du projet dans l'explorateur de solutions. Choisissez la commande Ajouter | Nouvel élément ... pour ajouter un nouvel élément de parure de vue. Choisissez Extensibilité | Éditeur dans le volet de navigation de gauche et choisissez Ornement de la fenêtre de l'éditeur dans le volet de droite. Saisissez le nom ColumnGuideAdornment comme nom d'élément et sélectionnez Add pour l'ajouter.
Vous pouvez voir que ce modèle d'élément a ajouté deux fichiers au projet (ainsi que des références, etc.) : ColumnGuideAdornment.cs et ColumnGuideAdornmentTextViewCreationListener.cs. Les modèles dessinent un rectangle violet sur la vue. Dans la section suivante, vous modifiez quelques lignes dans l'écouteur de création de vue et remplacez le contenu de ColumnGuideAdornment.cs.
Commandes. Dans l'Explorateur de solutions, appuyez sur le bouton droit de la souris sur le nœud du projet. Choisissez la commande Ajouter | Nouvel élément ... pour ajouter un nouvel élément de parure de vue. Choisissez Extensibilité | VSPackage dans le volet de navigation de gauche et choisissez Commande personnalisée dans le volet de droite. Saisissez le nom ColumnGuideCommands comme nom d'élément et sélectionnez Ajouter. En plus de plusieurs références, l'ajout des commandes et du package a également ajouté ColumnGuideCommands.cs, ColumnGuideCommandsPackage.cs, et ColumnGuideCommandsPackage.vsct. Dans la section suivante, vous remplacez le contenu du premier et du dernier fichier pour définir et mettre en œuvre les commandes.
Configuration de l'auditeur de création de la vue de texte
Ouvrez ColumnGuideAdornmentTextViewCreationListener.cs dans l'éditeur. Ce code met en œuvre un gestionnaire pour chaque fois que Visual Studio crée des vues de texte. Il existe des attributs qui contrôlent le moment où le gestionnaire est appelé en fonction des caractéristiques de la vue.
Le code doit également déclarer une couche d'ornement. Lorsque l'éditeur met à jour les vues, il récupère les couches d'ornement pour la vue et, à partir de là, les éléments d'ornement. Vous pouvez déclarer l'ordre de votre couche par rapport aux autres à l'aide d'attributs. Remplacez la ligne suivante :
[Order(After = PredefinedAdornmentLayers.Caret)]
avec ces deux lignes :
[Order(Before = PredefinedAdornmentLayers.Text)]
[TextViewRole(PredefinedTextViewRoles.Document)]
La ligne que vous avez remplacée fait partie d'un groupe d'attributs qui déclarent une couche d'ornement. La première ligne que vous avez changée ne modifie que l'emplacement des lignes de guidage de la colonne. Le fait de dessiner les lignes « avant » le texte dans la vue signifie qu'elles apparaissent derrière ou sous le texte. La deuxième ligne déclare que les ornements de guide de colonne sont applicables aux entités textuelles qui correspondent à votre notion de document, mais vous pourriez déclarer que l'ornement ne fonctionne, par exemple, que pour le texte éditable. Vous trouverez plus d'informations dans les points d'extension du service linguistique et de l'éditeur.
Implémenter le gestionnaire de paramètres
Remplacez le contenu du fichier GuidesSettingsManager.cs par le code suivant (expliqué ci-dessous) :
using Microsoft.VisualStudio.Settings;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Settings;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media;
namespace ColumnGuides
{
internal static class GuidesSettingsManager
{
// Because my code is always called from the UI thred, this succeeds.
internal static SettingsManager VsManagedSettingsManager =
new ShellSettingsManager(ServiceProvider.GlobalProvider);
private const int _maxGuides = 5;
private const string _collectionSettingsName = "Text Editor";
private const string _settingName = "Guides";
// 1000 seems reasonable since primary scenario is long lines of code
private const int _maxColumn = 1000;
static internal bool AddGuideline(int column)
{
if (! IsValidColumn(column))
throw new ArgumentOutOfRangeException(
"column",
"The parameter must be between 1 and " + _maxGuides.ToString());
var offsets = GuidesSettingsManager.GetColumnOffsets();
if (offsets.Count() >= _maxGuides)
return false;
// Check for duplicates
if (offsets.Contains(column))
return false;
offsets.Add(column);
WriteSettings(GuidesSettingsManager.GuidelinesColor, offsets);
return true;
}
static internal bool RemoveGuideline(int column)
{
if (!IsValidColumn(column))
throw new ArgumentOutOfRangeException(
"column", "The 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;
}
}
La majeure partie de ce code crée et analyse le format des paramètres : « RGB(<int>,<int>,<int>) <int>, <int>, ... ». Les nombres entiers à la fin sont les colonnes à base unique pour lesquelles vous souhaitez des guides de colonne. L'extension des guides de colonne capture tous ses paramètres dans une seule chaîne de valeur de paramètre.
Certaines parties du code méritent d'être soulignées. La ligne de code suivante permet d'obtenir le wrapper géré par Visual Studio pour le stockage des paramètres. Pour l'essentiel, il s'agit d'une abstraction du registre Windows, mais cette API est indépendante du mécanisme de stockage.
internal static SettingsManager VsManagedSettingsManager =
new ShellSettingsManager(ServiceProvider.GlobalProvider);
Le stockage des paramètres de Visual Studio utilise un identifiant de catégorie et un identifiant de paramètre pour identifier de manière unique tous les paramètres :
private const string _collectionSettingsName = "Text Editor";
private const string _settingName = "Guides";
Vous n'êtes pas obligé d'utiliser "Text Editor"
comme nom de catégorie. Vous pouvez choisir ce que vous voulez.
Les premières fonctions sont les points d'entrée pour modifier les paramètres. Elles vérifient les contraintes de haut niveau, comme le nombre maximum de guides autorisés. Ils appellent ensuite WriteSettings
, qui compose une chaîne de paramètres et définit la propriété GuideLinesConfiguration
. La définition de cette propriété enregistre la valeur des paramètres dans le magasin de paramètres de Visual Studio et déclenche l'événement SettingsChanged
pour mettre à jour tous les objets ColumnGuideAdornment
, chacun associé à une vue de texte.
Il existe quelques fonctions de point d'entrée, telles que CanAddGuideline
, qui sont utilisées pour mettre en œuvre des commandes qui modifient les paramètres. Lorsque Visual Studio affiche des menus, il interroge les implémentations de commandes pour savoir si la commande est actuellement activée, quel est son nom, etc. Vous verrez ci-dessous comment connecter ces points d'entrée pour les implémentations de commandes. Pour plus d'informations sur les commandes, voir Étendre les menus et les commandes.
Implémentez la classe ColumnGuideAdornment
La classe ColumnGuideAdornment
est instanciée pour chaque vue de texte qui peut avoir des ornements. Cette classe est à l'écoute des événements relatifs à la modification de la vue ou des paramètres, ainsi qu'à la mise à jour ou au redécoupage des guides de colonne, le cas échéant.
Remplacez le contenu du fichier ColumnGuideAdornment.cs par le code suivant (expliqué ci-dessous) :
using System;
using System.Windows.Media;
using Microsoft.VisualStudio.Text.Editor;
using System.Collections.Generic;
using System.Windows.Shapes;
using Microsoft.VisualStudio.Text.Formatting;
using System.Windows;
namespace ColumnGuides
{
/// <summary>
/// Adornment class, one instance per text view that draws a guides on the viewport
/// </summary>
internal sealed class ColumnGuideAdornment
{
private const double _lineThickness = 1.0;
private IList<Line> _guidelines;
private IWpfTextView _view;
private double _baseIndentation;
private double _columnWidth;
/// <summary>
/// Creates editor column guidelines
/// </summary>
/// <param name="view">The <see cref="IWpfTextView"/> upon
/// which the adornment will be drawn</param>
public ColumnGuideAdornment(IWpfTextView view)
{
_view = view;
_guidelines = CreateGuidelines();
GuidesSettingsManager.SettingsChanged +=
new GuidesSettingsManager.SettingsChangedHandler(SettingsChanged);
view.LayoutChanged +=
new EventHandler<TextViewLayoutChangedEventArgs>(OnViewLayoutChanged);
_view.Closed += new EventHandler(OnViewClosed);
}
void SettingsChanged()
{
_guidelines = CreateGuidelines();
UpdatePositions();
AddGuidelinesToAdornmentLayer();
}
void OnViewClosed(object sender, EventArgs e)
{
_view.LayoutChanged -= OnViewLayoutChanged;
_view.Closed -= OnViewClosed;
GuidesSettingsManager.SettingsChanged -= SettingsChanged;
}
private bool _firstLayoutDone;
void OnViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
{
bool fUpdatePositions = false;
IFormattedLineSource lineSource = _view.FormattedLineSource;
if (lineSource == null)
{
return;
}
if (_columnWidth != lineSource.ColumnWidth)
{
_columnWidth = lineSource.ColumnWidth;
fUpdatePositions = true;
}
if (_baseIndentation != lineSource.BaseIndentation)
{
_baseIndentation = lineSource.BaseIndentation;
fUpdatePositions = true;
}
if (fUpdatePositions ||
e.VerticalTranslation ||
e.NewViewState.ViewportTop != e.OldViewState.ViewportTop ||
e.NewViewState.ViewportBottom != e.OldViewState.ViewportBottom)
{
UpdatePositions();
}
if (!_firstLayoutDone)
{
AddGuidelinesToAdornmentLayer();
_firstLayoutDone = true;
}
}
private static IList<Line> CreateGuidelines()
{
Brush lineBrush = new SolidColorBrush(GuidesSettingsManager.GuidelinesColor);
DoubleCollection dashArray = new DoubleCollection(new double[] { 1.0, 3.0 });
IList<Line> result = new List<Line>();
foreach (int column in GuidesSettingsManager.GetColumnOffsets())
{
Line line = new Line()
{
// Use the DataContext slot as a cookie to hold the column
DataContext = column,
Stroke = lineBrush,
StrokeThickness = _lineThickness,
StrokeDashArray = dashArray
};
result.Add(line);
}
return result;
}
void UpdatePositions()
{
foreach (Line line in _guidelines)
{
int column = (int)line.DataContext;
line.X2 = _baseIndentation + 0.5 + column * _columnWidth;
line.X1 = line.X2;
line.Y1 = _view.ViewportTop;
line.Y2 = _view.ViewportBottom;
}
}
void AddGuidelinesToAdornmentLayer()
{
// Grab a reference to the adornment layer that this adornment
// should be added to
// Must match exported name in ColumnGuideAdornmentTextViewCreationListener
IAdornmentLayer adornmentLayer =
_view.GetAdornmentLayer("ColumnGuideAdornment");
if (adornmentLayer == null)
return;
adornmentLayer.RemoveAllAdornments();
// Add the guidelines to the adornment layer and make them relative
// to the viewport
foreach (UIElement element in _guidelines)
adornmentLayer.AddAdornment(AdornmentPositioningBehavior.OwnerControlled,
null, null, element, null);
}
}
}
Les instances de cette classe contiennent l'objet IWpfTextView associé et une liste d'objets Line
dessinés sur la vue.
Le constructeur (appelé à partir de ColumnGuideAdornmentTextViewCreationListener
lorsque Visual Studio crée de nouvelles vues) crée les objets guide de colonne Line
. Le constructeur ajoute également des gestionnaires pour l'événement SettingsChanged
(défini dans GuidesSettingsManager
) et les événements de vue LayoutChanged
et Closed
.
L'événement LayoutChanged
se déclenche à la suite de plusieurs types de changements dans la vue, notamment lorsque Visual Studio crée la vue. Le gestionnaire OnViewLayoutChanged
appelle AddGuidelinesToAdornmentLayer
à l'exécution. Le code de OnViewLayoutChanged
détermine s'il doit mettre à jour la position des lignes en fonction de changements tels que la taille des polices, les gouttières, le défilement horizontal, etc. Le code dans UpdatePositions
fait en sorte que des lignes de guidage soient tracées entre les caractères ou juste après la colonne de texte qui se trouve dans le décalage de caractères spécifié dans la ligne de texte.
Lorsque les paramètres changent, la fonction SettingsChanged
recrée tous les objets Line
avec les nouveaux paramètres. Après avoir défini la position des lignes, le code supprime tous les objets Line
précédents de la couche d'ornement ColumnGuideAdornment
et ajoute les nouveaux objets.
Définissez les commandes, les menus et leur emplacement.
La déclaration des commandes et des menus, le placement de groupes de commandes ou de menus sur d'autres menus et la mise en place de gestionnaires de commandes peuvent s'avérer très complexes. Cette procédure-à-pas met en évidence le fonctionnement des commandes dans cette extension, mais pour plus d'informations, consultez Étendre les menus et les commandes.
Introduction au code
L'extension Column Guides montre comment déclarer un groupe de commandes qui vont ensemble (ajouter une colonne, supprimer une colonne, changer la couleur de la ligne), puis placer ce groupe dans un sous-menu du menu contextuel de l'éditeur. L'extension Guides de colonnes ajoute également les commandes au menu principal Édition, mais les garde invisibles, comme le montre le modèle commun ci-dessous.
L'implémentation des commandes comporte trois parties : ColumnGuideCommandsPackage.cs, ColumnGuideCommandsPackage.vsct, et ColumnGuideCommands.cs. Le code généré par les modèles place une commande dans le menu Outils qui fait apparaître une boîte de dialogue en guise de mise en œuvre. Vous pouvez regarder comment cela est mis en œuvre dans les fichiers .vsct et ColumnGuideCommands.cs, car c'est simple. Vous remplacez le code de ces fichiers ci-dessous.
Le code du package contient les déclarations de base nécessaires pour que Visual Studio découvre que l'extension propose des commandes et trouve l'endroit où placer ces commandes. Lorsque le package s'initialise, il instancie la classe d'implémentation des commandes. Pour plus d'informations sur les packages relatifs aux commandes, voir Étendre les menus et les commandes.
Un modèle de commandes commun
Les commandes de l'extension Column Guides sont un exemple de modèle très courant dans Visual Studio. Vous placez les commandes apparentées dans un groupe et vous placez ce groupe dans un menu principal, souvent avec « <CommandFlag>CommandWellOnly</CommandFlag>
» pour rendre la commande invisible. En plaçant les commandes dans les menus principaux (comme Édition), vous leur donnez des noms sympathiques (comme Edition.AddColumnGuide), qui sont utiles pour retrouver les commandes lors de la réaffectation des raccourcis clavier dans Outils Options. Il est également utile pour obtenir la finalisation des commandes lorsque vous les invoquez à partir de la fenêtre de commande.
Vous ajoutez ensuite le groupe de commandes aux menus contextuels ou aux sous-menus dans lesquels vous pensez que l'utilisateur utilisera les commandes. Visual Studio considère CommandWellOnly
comme un indicateur d'invisibilité pour les menus principaux uniquement. Lorsque vous placez le même groupe de commandes dans un menu contextuel ou un sous-menu, les commandes sont visibles.
Dans le cadre du modèle commun, l'extension Guides de colonnes crée un deuxième groupe qui contient un seul sous-menu. Le sous-menu contient à son tour le premier groupe avec les commandes du guide des quatre colonnes. Le deuxième groupe contenant le sous-menu est la ressource réutilisable que vous placez dans divers menus contextuels, ce qui crée un sous-menu dans ces menus contextuels.
Le fichier .vsct
Le fichier .vsct déclare les commandes et leur emplacement, ainsi que les icônes, etc. Remplacez le contenu du fichier .vsct par le code suivant (expliqué ci-dessous) :
<?xml version="1.0" encoding="utf-8"?>
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This is the file that defines the actual layout and type of the commands.
It is divided in different sections (e.g. command definition, command
placement, ...), with each defining a specific set of properties.
See the comment before each section for more details about how to
use it. -->
<!-- The VSCT compiler (the tool that translates this file into the binary
format that VisualStudio will consume) has the ability to run a preprocessor
on the vsct file; this preprocessor is (usually) the C++ preprocessor, so
it is possible to define includes and macros with the same syntax used
in C++ files. Using this ability of the compiler here, we include some files
defining some of the constants that we will use inside the file. -->
<!--This is the file that defines the IDs for all the commands exposed by
VisualStudio. -->
<Extern href="stdidcmd.h"/>
<!--This header contains the command ids for the menus provided by the shell. -->
<Extern href="vsshlids.h"/>
<!--The Commands section is where commands, menus, and menu groups are defined.
This section uses a Guid to identify the package that provides the command
defined inside it. -->
<Commands package="guidColumnGuideCommandsPkg">
<!-- Inside this section we have different sub-sections: one for the menus, another
for the menu groups, one for the buttons (the actual commands), one for the combos
and the last one for the bitmaps used. Each element is identified by a command id
that is a unique pair of guid and numeric identifier; the guid part of the identifier
is usually called "command set" and is used to group different command inside a
logically related group; your package should define its own command set in order to
avoid collisions with command ids defined by other packages. -->
<!-- In this section you can define new menu groups. A menu group is a container for
other menus or buttons (commands); from a visual point of view you can see the
group as the part of a menu contained between two lines. The parent of a group
must be a menu. -->
<Groups>
<!-- The main group is parented to the edit menu. All the buttons within the group
have the "CommandWellOnly" flag, so they're actually invisible, but it means
they get canonical names that begin with "Edit". Using placements, the group
is also placed in the GuidesSubMenu group. -->
<!-- The priority 0xB801 is chosen so it goes just after
IDG_VS_EDIT_COMMANDWELL -->
<Group guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0xB801">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_EDIT" />
</Group>
<!-- Group for holding the "Guidelines" sub-menu anchor (the item on the menu that
drops the sub menu). The group is parented to
the context menu for code windows. That takes care of most editors, but it's
also placed in a couple of other windows using Placements -->
<Group guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN" />
</Group>
</Groups>
<Menus>
<Menu guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" priority="0x1000"
type="Menu">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup" />
<Strings>
<ButtonText>&Column Guides</ButtonText>
</Strings>
</Menu>
</Menus>
<!--Buttons section. -->
<!--This section defines the elements the user can interact with, like a menu command or a button
or combo box in a toolbar. -->
<Buttons>
<!--To define a menu group you have to specify its ID, the parent menu and its
display priority.
The command is visible and enabled by default. If you need to change the
visibility, status, etc, you can use the CommandFlag node.
You can add more than one CommandFlag node e.g.:
<CommandFlag>DefaultInvisible</CommandFlag>
<CommandFlag>DynamicVisibility</CommandFlag>
If you do not want an image next to your command, remove the Icon node or
set it to <Icon guid="guidOfficeIcon" id="msotcidNoIcon" /> -->
<Button guid="guidColumnGuidesCommandSet" id="cmdidAddColumnGuide"
priority="0x0100" type="Button">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
<Icon guid="guidImages" id="bmpPicAddGuide" />
<CommandFlag>CommandWellOnly</CommandFlag>
<CommandFlag>AllowParams</CommandFlag>
<Strings>
<ButtonText>&Add Column Guide</ButtonText>
</Strings>
</Button>
<Button guid="guidColumnGuidesCommandSet" id="cmdidRemoveColumnGuide"
priority="0x0101" type="Button">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
<Icon guid="guidImages" id="bmpPicRemoveGuide" />
<CommandFlag>CommandWellOnly</CommandFlag>
<CommandFlag>AllowParams</CommandFlag>
<Strings>
<ButtonText>&Remove Column Guide</ButtonText>
</Strings>
</Button>
<Button guid="guidColumnGuidesCommandSet" id="cmdidChooseGuideColor"
priority="0x0103" type="Button">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
<Icon guid="guidImages" id="bmpPicChooseColor" />
<CommandFlag>CommandWellOnly</CommandFlag>
<Strings>
<ButtonText>Column Guide &Color...</ButtonText>
</Strings>
</Button>
<Button guid="guidColumnGuidesCommandSet" id="cmdidRemoveAllColumnGuides"
priority="0x0102" type="Button">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
<CommandFlag>CommandWellOnly</CommandFlag>
<Strings>
<ButtonText>Remove A&ll Columns</ButtonText>
</Strings>
</Button>
</Buttons>
<!--The bitmaps section is used to define the bitmaps that are used for the
commands.-->
<Bitmaps>
<!-- The bitmap id is defined in a way that is a little bit different from the
others:
the declaration starts with a guid for the bitmap strip, then there is the
resource id of the bitmap strip containing the bitmaps and then there are
the numeric ids of the elements used inside a button definition. An important
aspect of this declaration is that the element id
must be the actual index (1-based) of the bitmap inside the bitmap strip. -->
<Bitmap guid="guidImages" href="Resources\ColumnGuideCommands.png"
usedList="bmpPicAddGuide, bmpPicRemoveGuide, bmpPicChooseColor" />
</Bitmaps>
</Commands>
<CommandPlacements>
<!-- Define secondary placements for our groups -->
<!-- Place the group containing the three commands in the sub-menu -->
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0x0100">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" />
</CommandPlacement>
<!-- The HTML editor context menu, for some reason, redefines its own groups
so we need to place a copy of our context menu there too. -->
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x1001">
<Parent guid="CMDSETID_HtmEdGrp" id="IDMX_HTM_SOURCE_HTML" />
</CommandPlacement>
<!-- The HTML context menu in Dev12 changed. -->
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x1001">
<Parent guid="CMDSETID_HtmEdGrp_Dev12" id="IDMX_HTM_SOURCE_HTML_Dev12" />
</CommandPlacement>
<!-- Similarly for Script -->
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x1001">
<Parent guid="CMDSETID_HtmEdGrp" id="IDMX_HTM_SOURCE_SCRIPT" />
</CommandPlacement>
<!-- Similarly for ASPX -->
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x1001">
<Parent guid="CMDSETID_HtmEdGrp" id="IDMX_HTM_SOURCE_ASPX" />
</CommandPlacement>
<!-- Similarly for the XAML editor context menu -->
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x0600">
<Parent guid="guidXamlUiCmds" id="IDM_XAML_EDITOR" />
</CommandPlacement>
</CommandPlacements>
<!-- This defines the identifiers and their values used above to index resources
and specify commands. -->
<Symbols>
<!-- This is the package guid. -->
<GuidSymbol name="guidColumnGuideCommandsPkg"
value="{e914e5de-0851-4904-b361-1a3a9d449704}" />
<!-- This is the guid used to group the menu commands together -->
<GuidSymbol name="guidColumnGuidesCommandSet"
value="{c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e}">
<IDSymbol name="GuidesContextMenuGroup" value="0x1020" />
<IDSymbol name="GuidesMenuItemsGroup" value="0x1021" />
<IDSymbol name="GuidesSubMenu" value="0x1022" />
<IDSymbol name="cmdidAddColumnGuide" value="0x0100" />
<IDSymbol name="cmdidRemoveColumnGuide" value="0x0101" />
<IDSymbol name="cmdidChooseGuideColor" value="0x0102" />
<IDSymbol name="cmdidRemoveAllColumnGuides" value="0x0103" />
</GuidSymbol>
<GuidSymbol name="guidImages" value="{2C99F852-587C-43AF-AA2D-F605DE2E46EF}">
<IDSymbol name="bmpPicAddGuide" value="1" />
<IDSymbol name="bmpPicRemoveGuide" value="2" />
<IDSymbol name="bmpPicChooseColor" value="3" />
</GuidSymbol>
<GuidSymbol name="CMDSETID_HtmEdGrp_Dev12"
value="{78F03954-2FB8-4087-8CE7-59D71710B3BB}">
<IDSymbol name="IDMX_HTM_SOURCE_HTML_Dev12" value="0x1" />
</GuidSymbol>
<GuidSymbol name="CMDSETID_HtmEdGrp" value="{d7e8c5e1-bdb8-11d0-9c88-0000f8040a53}">
<IDSymbol name="IDMX_HTM_SOURCE_HTML" value="0x33" />
<IDSymbol name="IDMX_HTM_SOURCE_SCRIPT" value="0x34" />
<IDSymbol name="IDMX_HTM_SOURCE_ASPX" value="0x35" />
</GuidSymbol>
<GuidSymbol name="guidXamlUiCmds" value="{4c87b692-1202-46aa-b64c-ef01faec53da}">
<IDSymbol name="IDM_XAML_EDITOR" value="0x103" />
</GuidSymbol>
</Symbols>
</CommandTable>
GUIDs. Pour que Visual Studio trouve vos gestionnaires de commandes et les invoque, vous devez vous assurer que le GUID du package déclaré dans le fichier ColumnGuideCommandsPackage.cs (généré à partir du modèle d'élément de projet) correspond au GUID du package déclaré dans le fichier .vsct (copié depuis plus haut). Si vous réutilisez cet exemple de code, assurez-vous d'avoir un GUID différent afin de ne pas entrer en conflit avec d'autres personnes qui auraient copié ce code.
Trouvez cette ligne dans ColumnGuideCommandsPackage.cs et copiez le GUID entre les guillemets :
public const string PackageGuidString = "ef726849-5447-4f73-8de5-01b9e930f7cd";
Ensuite, collez le GUID dans le fichier .vsct de sorte que vous ayez la ligne suivante dans vos déclarations Symbols
:
<GuidSymbol name="guidColumnGuideCommandsPkg"
value="{ef726849-5447-4f73-8de5-01b9e930f7cd}" />
Les GUID du jeu de commandes et du fichier image bitmap doivent également être uniques pour vos extensions :
<GuidSymbol name="guidColumnGuidesCommandSet"
value="{c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e}">
<GuidSymbol name="guidImages" value="{2C99F852-587C-43AF-AA2D-F605DE2E46EF}">
Cependant, vous n'avez pas besoin de modifier les GUID du jeu de commandes et de l'image bitmap dans cette procédure-à-pas pour que le code fonctionne. Le GUID du jeu de commandes doit correspondre à la déclaration du fichier ColumnGuideCommands.cs, mais vous remplacez également le contenu de ce fichier; par conséquent, les GUID correspondront.
Les autres GUID du fichier .vsct identifient des menus préexistants auxquels sont ajoutées les commandes du guide des colonnes, de sorte qu'ils ne changent jamais.
Sections du dossier. Le fichier .vsct comporte trois sections extérieures : les commandes, les placements et les symboles. La section des commandes définit les groupes de commandes, les menus, les boutons ou les éléments de menu, ainsi que les images bitmap pour les icônes. La section des placements indique l'emplacement des groupes dans les menus ou les placements supplémentaires dans des menus préexistants. La section des symboles déclare les identifiants utilisés ailleurs dans le fichier .vsct, ce qui rend le code .vsct plus lisible que s'il contenait des GUID et des nombres hexadécimaux partout.
Section des commandes, définitions des groupes. La section des commandes définit d'abord les groupes de commandes. Les groupes de commandes sont les commandes que vous voyez dans les menus avec de légères lignes grises séparant les groupes. Un groupe peut également remplir un sous-menu entier, comme dans cet exemple, et vous ne voyez pas les lignes grises de séparation dans ce cas. Les fichiers .vsct déclarent deux groupes, le GuidesMenuItemsGroup
qui est relié au IDM_VS_MENU_EDIT
(le menu principal Éditer) et le GuidesContextMenuGroup
qui est relié au IDM_VS_CTXT_CODEWIN
(le menu contextuel de l'éditeur de code).
La deuxième déclaration de groupe a une priorité 0x0600
:
<Group guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x0600">
L'idée est de placer le sous-menu des guides de colonne à la fin de tout menu contextuel auquel vous ajoutez le groupe de sous-menus. Mais vous ne devez pas partir du principe que vous savez mieux que quiconque et forcer le sous-menu à toujours passer en dernier en utilisant une priorité de 0xFFFF
. Vous devez expérimenter avec le nombre pour voir où se trouve votre sous-menu dans les menus contextuels où vous le placez. Dans ce cas, 0x0600
est suffisamment haut pour être placé à la fin des menus pour autant que vous puissiez le voir, mais cela laisse de la place pour que quelqu'un d'autre conçoive son extension de manière à ce qu'elle soit plus basse que l'extension des guides de colonne si cela est souhaitable.
Section Commandes, définition du menu. Ensuite, la section des commandes définit le sous-menu GuidesSubMenu
, parent du GuidesContextMenuGroup
. Le groupe GuidesContextMenuGroup
est le groupe que vous ajoutez à tous les menus contextuels pertinents. Dans la section des placements, le code place le groupe avec les commandes de guides à quatre colonnes dans ce sous-menu.
Section Commandes, définitions des boutons. La section des commandes définit ensuite les éléments de menu ou les boutons qui correspondent aux commandes des guides à quatre colonnes. CommandWellOnly
, mentionnée ci-dessus, signifie que les commandes sont invisibles lorsqu'elles sont placées dans un menu principal. Deux des déclarations de boutons d'éléments de menu (ajouter un guide et supprimer un guide) ont également un indicateur AllowParams
:
<CommandFlag>AllowParams</CommandFlag>
Cet indicateur permet, outre le placement dans le menu principal, à la commande de recevoir des arguments lorsque Visual Studio invoque le gestionnaire de commande. Si l'utilisateur exécute la commande à partir de la fenêtre de commande, l'argument est transmis au gestionnaire de commande dans les arguments de l'événement.
Sections de commande, définitions de bitmaps. Enfin, la section des commandes déclare les bitmaps ou icônes utilisés pour les commandes. Cette section est une simple déclaration qui identifie la ressource du projet et énumère les index à une base des icônes utilisées. La section symboles du fichier .vsct déclare les valeurs des identifiants utilisés comme index. Cette procédure-à-pas utilise la bande bitmap fournie avec le modèle d'élément de commande personnalisé ajouté au projet.
Section Placements. Après la section des commandes se trouve la section des placements. La première est celle où le code ajoute le premier groupe mentionné ci-dessus, qui contient les commandes de guidage sur quatre colonnes, au sous-menu dans lequel les commandes apparaissent :
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0x0100">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" />
</CommandPlacement>
Tous les autres placements ajoutent le GuidesContextMenuGroup
(qui contient le GuidesSubMenu
) aux autres menus contextuels de l'éditeur. Lorsque le code a déclaré le GuidesContextMenuGroup
, il a été rattaché au menu contextuel de l'éditeur de code. C'est pourquoi vous ne voyez pas de placement pour le menu contextuel de l'éditeur de code.
Section des symboles. Comme indiqué plus haut, la section des symboles déclare les identifiants utilisés ailleurs dans le fichier .vsct, ce qui rend le code .vsct plus lisible que d'avoir des GUID et des nombres hexadécimaux partout. Les points importants de cette section sont que le GUID du package doit correspondre à la déclaration de la classe du package. Et le GUID du jeu de commandes doit correspondre à la déclaration de la classe d'implémentation des commandes.
Implémentation des commandes
Le fichier ColumnGuideCommands.cs met en œuvre les commandes et connecte les gestionnaires. Lorsque Visual Studio charge le package et l'initialise, le package appelle à son tour Initialize
la classe de mise en œuvre des commandes. L'initialisation des commandes instancie simplement la classe, et le constructeur connecte tous les gestionnaires de commandes.
Remplacez le contenu du fichier ColumnGuideCommands.cs par le code suivant (expliqué ci-dessous) :
using System;
using System.ComponentModel.Design;
using System.Globalization;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio;
namespace ColumnGuides
{
/// <summary>
/// Command handler
/// </summary>
internal sealed class ColumnGuideCommands
{
const int cmdidAddColumnGuide = 0x0100;
const int cmdidRemoveColumnGuide = 0x0101;
const int cmdidChooseGuideColor = 0x0102;
const int cmdidRemoveAllColumnGuides = 0x0103;
/// <summary>
/// Command menu group (command set GUID).
/// </summary>
static readonly Guid CommandSet =
new Guid("c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e");
/// <summary>
/// VS Package that provides this command, not null.
/// </summary>
private readonly Package package;
OleMenuCommand _addGuidelineCommand;
OleMenuCommand _removeGuidelineCommand;
/// <summary>
/// Initializes the singleton instance of the command.
/// </summary>
/// <param name="package">Owner package, not null.</param>
public static void Initialize(Package package)
{
Instance = new ColumnGuideCommands(package);
}
/// <summary>
/// Gets the instance of the command.
/// </summary>
public static ColumnGuideCommands Instance
{
get;
private set;
}
/// <summary>
/// Initializes a new instance of the <see cref="ColumnGuideCommands"/> class.
/// Adds our command handlers for menu (commands must exist in the command
/// table file)
/// </summary>
/// <param name="package">Owner package, not null.</param>
private ColumnGuideCommands(Package package)
{
if (package == null)
{
throw new ArgumentNullException("package");
}
this.package = package;
// Add our command handlers for menu (commands must exist in the .vsct file)
OleMenuCommandService commandService =
this.ServiceProvider.GetService(typeof(IMenuCommandService))
as OleMenuCommandService;
if (commandService != null)
{
// Add guide
_addGuidelineCommand =
new OleMenuCommand(AddColumnGuideExecuted, null,
AddColumnGuideBeforeQueryStatus,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidAddColumnGuide));
_addGuidelineCommand.ParametersDescription = "<column>";
commandService.AddCommand(_addGuidelineCommand);
// Remove guide
_removeGuidelineCommand =
new OleMenuCommand(RemoveColumnGuideExecuted, null,
RemoveColumnGuideBeforeQueryStatus,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidRemoveColumnGuide));
_removeGuidelineCommand.ParametersDescription = "<column>";
commandService.AddCommand(_removeGuidelineCommand);
// Choose color
commandService.AddCommand(
new MenuCommand(ChooseGuideColorExecuted,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidChooseGuideColor)));
// Remove all
commandService.AddCommand(
new MenuCommand(RemoveAllGuidelinesExecuted,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidRemoveAllColumnGuides)));
}
}
/// <summary>
/// Gets the service provider from the owner package.
/// </summary>
private IServiceProvider ServiceProvider
{
get
{
return this.package;
}
}
private void AddColumnGuideBeforeQueryStatus(object sender, EventArgs e)
{
int currentColumn = GetCurrentEditorColumn();
_addGuidelineCommand.Enabled =
GuidesSettingsManager.CanAddGuideline(currentColumn);
}
private void RemoveColumnGuideBeforeQueryStatus(object sender, EventArgs e)
{
int currentColumn = GetCurrentEditorColumn();
_removeGuidelineCommand.Enabled =
GuidesSettingsManager.CanRemoveGuideline(currentColumn);
}
private int GetCurrentEditorColumn()
{
IVsTextView view = GetActiveTextView();
if (view == null)
{
return -1;
}
try
{
IWpfTextView textView = GetTextViewFromVsTextView(view);
int column = GetCaretColumn(textView);
// Note: GetCaretColumn returns 0-based positions. Guidelines are 1-based
// positions.
// However, do not subtract one here since the caret is positioned to the
// left of
// the given column and the guidelines are positioned to the right. We
// want the
// guideline to line up with the current caret position. e.g. When the
// caret is
// at position 1 (zero-based), the status bar says column 2. We want to
// add a
// guideline for column 1 since that will place the guideline where the
// caret is.
return column;
}
catch (InvalidOperationException)
{
return -1;
}
}
/// <summary>
/// Find the active text view (if any) in the active document.
/// </summary>
/// <returns>The IVsTextView of the active view, or null if there is no active
/// document or the
/// active view in the active document is not a text view.</returns>
private IVsTextView GetActiveTextView()
{
IVsMonitorSelection selection =
this.ServiceProvider.GetService(typeof(IVsMonitorSelection))
as IVsMonitorSelection;
object frameObj = null;
ErrorHandler.ThrowOnFailure(
selection.GetCurrentElementValue(
(uint)VSConstants.VSSELELEMID.SEID_DocumentFrame, out frameObj));
IVsWindowFrame frame = frameObj as IVsWindowFrame;
if (frame == null)
{
return null;
}
return GetActiveView(frame);
}
private static IVsTextView GetActiveView(IVsWindowFrame windowFrame)
{
if (windowFrame == null)
{
throw new ArgumentException("windowFrame");
}
object pvar;
ErrorHandler.ThrowOnFailure(
windowFrame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView, out pvar));
IVsTextView textView = pvar as IVsTextView;
if (textView == null)
{
IVsCodeWindow codeWin = pvar as IVsCodeWindow;
if (codeWin != null)
{
ErrorHandler.ThrowOnFailure(codeWin.GetLastActiveView(out textView));
}
}
return textView;
}
private static IWpfTextView GetTextViewFromVsTextView(IVsTextView view)
{
if (view == null)
{
throw new ArgumentNullException("view");
}
IVsUserData userData = view as IVsUserData;
if (userData == null)
{
throw new InvalidOperationException();
}
object objTextViewHost;
if (VSConstants.S_OK
!= userData.GetData(Microsoft.VisualStudio
.Editor
.DefGuidList.guidIWpfTextViewHost,
out objTextViewHost))
{
throw new InvalidOperationException();
}
IWpfTextViewHost textViewHost = objTextViewHost as IWpfTextViewHost;
if (textViewHost == null)
{
throw new InvalidOperationException();
}
return textViewHost.TextView;
}
/// <summary>
/// Given an IWpfTextView, find the position of the caret and report its column
/// number. The column number is 0-based
/// </summary>
/// <param name="textView">The text view containing the caret</param>
/// <returns>The column number of the caret's position. When the caret is at the
/// leftmost column, the return value is zero.</returns>
private static int GetCaretColumn(IWpfTextView textView)
{
// This is the code the editor uses to populate the status bar.
Microsoft.VisualStudio.Text.Formatting.ITextViewLine caretViewLine =
textView.Caret.ContainingTextViewLine;
double columnWidth = textView.FormattedLineSource.ColumnWidth;
return (int)(Math.Round((textView.Caret.Left - caretViewLine.Left)
/ columnWidth));
}
/// <summary>
/// Determine the applicable column number for an add or remove command.
/// The column is parsed from command arguments, if present. Otherwise
/// the current position of the caret is used to determine the column.
/// </summary>
/// <param name="e">Event args passed to the command handler.</param>
/// <returns>The column number. May be negative to indicate the column number is
/// unavailable.</returns>
/// <exception cref="ArgumentException">The column number parsed from event args
/// was not a valid integer.</exception>
private int GetApplicableColumn(EventArgs e)
{
var inValue = ((OleMenuCmdEventArgs)e).InValue as string;
if (!string.IsNullOrEmpty(inValue))
{
int column;
if (!int.TryParse(inValue, out column) || column < 0)
throw new ArgumentException("Invalid column");
return column;
}
return GetCurrentEditorColumn();
}
/// <summary>
/// This function is the callback used to execute a command when a menu item
/// is clicked. See the Initialize method to see how the menu item is associated
/// to this function using the OleMenuCommandService service and the MenuCommand
/// class.
/// </summary>
private void AddColumnGuideExecuted(object sender, EventArgs e)
{
int column = GetApplicableColumn(e);
if (column >= 0)
{
GuidesSettingsManager.AddGuideline(column);
}
}
private void RemoveColumnGuideExecuted(object sender, EventArgs e)
{
int column = GetApplicableColumn(e);
if (column >= 0)
{
GuidesSettingsManager.RemoveGuideline(column);
}
}
private void RemoveAllGuidelinesExecuted(object sender, EventArgs e)
{
GuidesSettingsManager.RemoveAllGuidelines();
}
private void ChooseGuideColorExecuted(object sender, EventArgs e)
{
System.Windows.Media.Color color = GuidesSettingsManager.GuidelinesColor;
using (System.Windows.Forms.ColorDialog picker =
new System.Windows.Forms.ColorDialog())
{
picker.Color = System.Drawing.Color.FromArgb(255, color.R, color.G,
color.B);
if (picker.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
GuidesSettingsManager.GuidelinesColor =
System.Windows.Media.Color.FromRgb(picker.Color.R,
picker.Color.G,
picker.Color.B);
}
}
}
}
}
Corrigez les références. Il vous manque une référence à ce stade. Appuyez sur le bouton droit de la souris sur le nœud References dans l'explorateur de solutions. Choisissez la commande Ajouter… La boîte de dialogue Ajouter une référence comporte un champ de recherche dans le coin supérieur droit. Saisissez « editor » (sans les guillemets). Choisissez l'élément Microsoft.VisualStudio.Editor (vous devez cocher la case à gauche de l'élément, et non simplement sélectionner l'élément) et cliquez sur OK pour ajouter la référence.
Initialisation. Lorsque la classe du package s'initialise, elle fait appel Initialize
à la classe de mise en œuvre des commandes. L'initialisation ColumnGuideCommands
instancie la classe et enregistre l'instance de la classe et la référence au package dans les membres de la classe.
Examinons l'un des gestionnaires de commandes du constructeur de la classe :
_addGuidelineCommand =
new OleMenuCommand(AddColumnGuideExecuted, null,
AddColumnGuideBeforeQueryStatus,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidAddColumnGuide));
Vous créez un OleMenuCommand
. Visual Studio utilise le système de commande de Microsoft Office. Les arguments clés lors de l'instanciation d'un OleMenuCommand
sont la fonction qui met en œuvre la commande (AddColumnGuideExecuted
), la fonction à appeler lorsque Visual Studio affiche un menu avec la commande (AddColumnGuideBeforeQueryStatus
), et l'ID de la commande. Visual Studio appelle la fonction d'état de la requête avant d'afficher une commande dans un menu afin que la commande puisse se rendre invisible ou grisée pour un affichage particulier du menu (par exemple, en désactivant Copier s'il n'y a pas de sélection), changer d'icône ou même de nom (par exemple, de Ajouter quelque chose à Supprimer quelque chose), et ainsi de suite. L'ID de la commande doit correspondre à un ID de commande déclaré dans le fichier .vsct. Les chaînes du jeu de commandes et de la commande d'ajout de guides de colonnes doivent correspondre entre le fichier .vsct et le fichier ColumnGuideCommands.cs.
La ligne suivante fournit une assistance lorsque les utilisateurs invoquent la commande via la fenêtre de commande (expliquée ci-dessous) :
_addGuidelineCommand.ParametersDescription = "<column>";
Statut de la requête. Les fonctions d'état d'interrogation AddColumnGuideBeforeQueryStatus
et RemoveColumnGuideBeforeQueryStatus
vérifient certains paramètres (tels que le nombre maximum de guides ou la colonne maximum) ou s'il y a un guide de colonne à retirer. Ils activent les commandes si les conditions sont réunies. Les fonctions d'état des requêtes doivent être efficaces, car elles s'exécutent chaque fois que Visual Studio affiche un menu et pour chaque commande du menu.
Fonction AddColumnGuideExecuted. La partie intéressante de l'ajout d'un guide consiste à déterminer la vue actuelle de l'éditeur et l'emplacement du curseur. Tout d'abord, cette fonction appelle GetApplicableColumn
, qui vérifie s'il y a un argument fourni par l'utilisateur dans les arguments d'événement du gestionnaire de commande, et s'il n'y en a pas, la fonction vérifie la vue de l'éditeur :
private int GetApplicableColumn(EventArgs e)
{
var inValue = ((OleMenuCmdEventArgs)e).InValue as string;
if (!string.IsNullOrEmpty(inValue))
{
int column;
if (!int.TryParse(inValue, out column) || column < 0)
throw new ArgumentException("Invalid column");
return column;
}
return GetCurrentEditorColumn();
}
GetCurrentEditorColumn
doit creuser un peu pour obtenir une IWpfTextView vue du code. Si vous suivez les étapes GetActiveTextView
, GetActiveView
et GetTextViewFromVsTextView
, vous verrez comment procéder. Le code suivant est le code abstrait pertinent, commençant par la sélection actuelle, puis obtenant le cadre de la sélection, puis obtenant le DocView du cadre en tant que IVsTextView, puis obtenant un IVsUserData de l'IVsTextView, puis obtenant un hôte de vue, et enfin l'IWpfTextView :
IVsMonitorSelection selection =
this.ServiceProvider.GetService(typeof(IVsMonitorSelection))
as IVsMonitorSelection;
object frameObj = null;
ErrorHandler.ThrowOnFailure(selection.GetCurrentElementValue(
(uint)VSConstants.VSSELELEMID.SEID_DocumentFrame,
out frameObj));
IVsWindowFrame frame = frameObj as IVsWindowFrame;
if (frame == null)
<<do nothing>>;
...
object pvar;
ErrorHandler.ThrowOnFailure(frame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView,
out pvar));
IVsTextView textView = pvar as IVsTextView;
if (textView == null)
{
IVsCodeWindow codeWin = pvar as IVsCodeWindow;
if (codeWin != null)
{
ErrorHandler.ThrowOnFailure(codeWin.GetLastActiveView(out textView));
}
}
...
if (textView == null)
<<do nothing>>
IVsUserData userData = textView as IVsUserData;
if (userData == null)
<<do nothing>>
object objTextViewHost;
if (VSConstants.S_OK
!= userData.GetData(Microsoft.VisualStudio.Editor.DefGuidList
.guidIWpfTextViewHost,
out objTextViewHost))
{
<<do nothing>>
}
IWpfTextViewHost textViewHost = objTextViewHost as IWpfTextViewHost;
if (textViewHost == null)
<<do nothing>>
IWpfTextView textView = textViewHost.TextView;
Une fois que vous avez un IWpfTextView, vous pouvez obtenir la colonne où se trouve le curseur :
private static int GetCaretColumn(IWpfTextView textView)
{
// This is the code the editor uses to populate the status bar.
Microsoft.VisualStudio.Text.Formatting.ITextViewLine caretViewLine =
textView.Caret.ContainingTextViewLine;
double columnWidth = textView.FormattedLineSource.ColumnWidth;
return (int)(Math.Round((textView.Caret.Left - caretViewLine.Left)
/ columnWidth));
}
Avec la colonne actuelle en main où l'utilisateur a cliqué, le code fait simplement appel au gestionnaire de paramètres pour ajouter ou supprimer la colonne. Le gestionnaire des paramètres déclenche l'événement que tous les objets ColumnGuideAdornment
écoutent. Lorsque l'événement se déclenche, ces objets mettent à jour leurs vues de texte associées avec les nouveaux paramètres de guidage de la colonne.
Invoquer une commande à partir de la fenêtre de commande
L'exemple de guides de colonne permet aux utilisateurs d'invoquer deux commandes à partir de la fenêtre de commande, ce qui constitue une forme d'extensibilité. Si vous utilisez la commande Affichage | Autres fenêtres | Fenêtre de commande, vous pouvez voir la fenêtre de commande. Vous pouvez interagir avec la fenêtre de commande en saisissant « edit. », et en complétant le nom de la commande et en fournissant l'argument 120, vous obtenez le résultat suivant :
> Edit.AddColumnGuide 120
>
Les éléments de l'exemple qui permettent ce comportement se trouvent dans les déclarations du fichier .vsct, dans le constructeur de la classe ColumnGuideCommands
lorsqu'il connecte les gestionnaires de commandes, et dans les implémentations des gestionnaires de commandes qui vérifient les arguments des événements.
Vous avez vu des « <CommandFlag>CommandWellOnly</CommandFlag>
» dans le fichier .vsct ainsi que des placements dans le menu principal Édition alors que les commandes ne sont pas affichées dans l'interface utilisateur du menu Édition. Le fait de les avoir dans le menu principal Édition leur donne des noms comme Edit.AddColumnGuide. La déclaration du groupe de commandes qui contient les quatre commandes place le groupe directement dans le menu Édition :
<Group guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0xB801">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_EDIT" />
</Group>
La section des boutons a ensuite déclaré les commandes CommandWellOnly
pour les rendre invisibles dans le menu principal et les a déclarées avec AllowParams
:
<Button guid="guidColumnGuidesCommandSet" id="cmdidAddColumnGuide"
priority="0x0100" type="Button">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
<Icon guid="guidImages" id="bmpPicAddGuide" />
<CommandFlag>CommandWellOnly</CommandFlag>
<CommandFlag>AllowParams</CommandFlag>
Vous avez vu que le code d'accrochage du gestionnaire de commande dans le constructeur de la classe ColumnGuideCommands
fournissait une description du paramètre autorisé :
_addGuidelineCommand.ParametersDescription = "<column>";
Vous avez vu que la fonction GetApplicableColumn
vérifie OleMenuCmdEventArgs
pour une valeur avant de vérifier la vue de l'éditeur pour une colonne courante :
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;
}
Essayez votre extension
Vous pouvez maintenant appuyer sur F5 pour exécuter votre extension de guides de colonnes. Ouvrez un fichier texte et utilisez le menu contextuel de l'éditeur pour ajouter des lignes de guidage, les supprimer et modifier leur couleur. Cliquez dans le texte (pas dans l'espace blanc passé en fin de ligne) pour ajouter un guide de colonne, ou l'éditeur l'ajoute à la dernière colonne de la ligne. Si vous utilisez la fenêtre de commande et que vous invoquez les commandes avec un argument, vous pouvez ajouter des guides de colonne n'importe où.
Si vous souhaitez essayer différents emplacements de commandes, modifier les noms, changer les icônes, etc., et si vous rencontrez des problèmes lorsque Visual Studio vous affiche le dernier code dans les menus, vous pouvez réinitialiser le répertoire de stockage expérimental dans lequel vous effectuez le débogage. Ouvrez le menu Démarrer de Windows et tapez « réinitialiser ». Recherchez et exécutez la commande Réinitialiser la prochaine instance expérimentale de Visual Studio. Cette commande nettoie le répertoire de stockage expérimental de tous les composants d'extension. Elle ne nettoie pas les paramètres des composants, de sorte que tous les guides que vous aviez lorsque vous avez fermé le répertoire de stockage expérimental de Visual Studio sont toujours présents lorsque votre code lit le magasin de paramètres lors du prochain lancement.
Projet de code terminé
Il y aura bientôt un projet GitHub d'échantillons de Visual Studio Extensibility, et le projet terminé s'y trouvera. Cet article sera mis à jour pour y faire référence lorsque cela se produira. Le projet d'exemple terminé peut avoir des guids différents et aura une bande de bitmaps différente pour les icônes de commande.
Vous pouvez tester une version de la fonctionnalité de guides de colonnes avec cette extension de la galerie Visual Studio.