Walkthrough: Create a view adornment, commands, and settings (column guides)
Applies to: Visual Studio Visual Studio for Mac
Note
This article applies to Visual Studio 2017. If you're looking for the latest Visual Studio documentation, see Visual Studio documentation. We recommend upgrading to the latest version of Visual Studio. Download it here
You can extend the Visual Studio text/code editor with commands and view effects. This article shows you how to get started with a popular extension feature, column guides. Column guides are visually light lines drawn on the text editor's view to help you manage your code to specific column widths. Specifically, formatted code can be important for samples you include in documents, blog posts, or bug reports.
In this walkthrough, you:
Create a VSIX project
Add an editor view adornment
Add support for saving and getting settings (where to draw column guides and their color)
Add commands (add/remove column guides, change their color)
Place the commands on the Edit menu and text document context menus
Add support for invoking the commands from the Visual Studio Command Window
You can try out a version of the column guides feature with this Visual Studio Galleryextension.
Note
In this walkthrough, you paste a great amount of code into a few files generated by Visual Studio extension templates. But, soon this walkthrough will refer to a completed solution on GitHub with other extension examples. The completed code is slightly different in that it has real command icons instead of using generictemplate icons.
Get started
Starting in Visual Studio 2015, you do not install the Visual Studio SDK from the download center. It's included as an optional feature in Visual Studio setup. You can also install the VS SDK later on. For more information, see Install the Visual Studio SDK.
Set up the solution
First, you create a VSIX project, add an editor view adornment, and then add a command (which adds a VSPackage to own the command). The basic architecture is as follows:
You have a text view creation listener that creates a
ColumnGuideAdornment
object per view. This object listens for events about the view changing or settings changing, updating or redrawing column guides as necessary.There's a
GuidesSettingsManager
that handles reading and writing from the Visual Studio settings storage. The settings manager also has operations for updating the settings that support the user commands (add column, remove column, change color).There's a VSIP package that's necessary if you have user commands, but it's just boilerplate code that initializes the commands implementation object.
There's a
ColumnGuideCommands
object that runs the user commands and hooks up the command handlers for commands declared in the .vsct file.VSIX. Use File | New ... command to create a project. Choose the Extensibility node under C# in the left navigation pane and choose VSIX Project in the right pane. Enter the name ColumnGuides and choose OK to create the project.
View adornment. Press the right pointer button on the project node in the Solution Explorer. Choose the Add | New Item ... command to add a new view adornment item. Choose Extensibility | Editor in the left navigation pane and choose Editor Viewport Adornment in the right pane. Enter the name ColumnGuideAdornment as the item name and choose Add to add it.
You can see this item template added two files to the project (as well as references, and so on): ColumnGuideAdornment.cs and ColumnGuideAdornmentTextViewCreationListener.cs. The templates draw a purple rectangle on the view. In the following section, you change a couple of lines in the view creation listener and replace the contents of ColumnGuideAdornment.cs.
Commands. In Solution Explorer, press the right pointer button on the project node. Choose the Add | New Item ... command to add a new view adornment item. Choose Extensibility | VSPackage in the left navigation pane and choose Custom Command in the right pane. Enter the name ColumnGuideCommands as the item name and choose Add. In addition to several references, adding the commands and package also added ColumnGuideCommands.cs, ColumnGuideCommandsPackage.cs, and ColumnGuideCommandsPackage.vsct. In the following section, you replace the contents of first and last files to define and implement the commands.
Set up the text view creation listener
Open ColumnGuideAdornmentTextViewCreationListener.cs in the editor. This code implements a handler for whenever Visual Studio creates text views. There are attributes that control when the handler is called depending on characteristics of the view.
The code also must declare an adornment layer. When the editor updates views, it gets the adornment layers for the view and from that gets the adornment elements. You can declare the ordering of your layer relative to others with attributes. Replace the following line:
[Order(After = PredefinedAdornmentLayers.Caret)]
with these two lines:
[Order(Before = PredefinedAdornmentLayers.Text)]
[TextViewRole(PredefinedTextViewRoles.Document)]
The line you replaced is in a group of attributes that declare an adornment layer. The first line you changed only changes where the column guide lines appear. Drawing the lines "before" the text in the view means they appear behind or below the text. The second line declares that the column guide adornments are applicable to text entities that fit your notion of a document, but you could declare the adornment, for example, to only work for editable text. There's more information in Language service and editor extension points
Implement the settings manager
Replace the contents of the GuidesSettingsManager.cs with the following code (explained below):
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 paramenter must be between 1 and " + _maxGuides.ToString());
var offsets = GuidesSettingsManager.GetColumnOffsets();
if (offsets.Count() >= _maxGuides)
return false;
// Check for duplicates
if (offsets.Contains(column))
return false;
offsets.Add(column);
WriteSettings(GuidesSettingsManager.GuidelinesColor, offsets);
return true;
}
static internal bool RemoveGuideline(int column)
{
if (!IsValidColumn(column))
throw new ArgumentOutOfRangeException(
"column", "The paramenter must be between 1 and 10,000");
var columns = GuidesSettingsManager.GetColumnOffsets();
if (! columns.Remove(column))
{
// Not present. Allow user to remove the last column
// even if they're not on the right column.
if (columns.Count != 1)
return false;
columns.Clear();
}
WriteSettings(GuidesSettingsManager.GuidelinesColor, columns);
return true;
}
static internal bool CanAddGuideline(int column)
{
if (!IsValidColumn(column))
return false;
var offsets = GetColumnOffsets();
if (offsets.Count >= _maxGuides)
return false;
return ! offsets.Contains(column);
}
static internal bool CanRemoveGuideline(int column)
{
if (! IsValidColumn(column))
return false;
// Allow user to remove the last guideline regardless of the column.
// Okay to call count, we limit the number of guides.
var offsets = GuidesSettingsManager.GetColumnOffsets();
return offsets.Contains(column) || offsets.Count() == 1;
}
static internal void RemoveAllGuidelines()
{
WriteSettings(GuidesSettingsManager.GuidelinesColor, new int[0]);
}
private static bool IsValidColumn(int column)
{
// zero is allowed (per user request)
return 0 <= column && column <= _maxColumn;
}
// This has format "RGB(<int>, <int>, <int>) <int> <int>...".
// There can be any number of ints following the RGB part,
// and each int is a column (char offset into line) where to draw.
static private string _guidelinesConfiguration;
static private string GuidelinesConfiguration
{
get
{
if (_guidelinesConfiguration == null)
{
_guidelinesConfiguration =
GetUserSettingsString(
GuidesSettingsManager._collectionSettingsName,
GuidesSettingsManager._settingName)
.Trim();
}
return _guidelinesConfiguration;
}
set
{
if (value != _guidelinesConfiguration)
{
_guidelinesConfiguration = value;
WriteUserSettingsString(
GuidesSettingsManager._collectionSettingsName,
GuidesSettingsManager._settingName, value);
// Notify ColumnGuideAdornments to update adornments in views.
var handler = GuidesSettingsManager.SettingsChanged;
if (handler != null)
handler();
}
}
}
internal static string GetUserSettingsString(string collection, string setting)
{
var store = GuidesSettingsManager
.VsManagedSettingsManager
.GetReadOnlySettingsStore(SettingsScope.UserSettings);
return store.GetString(collection, setting, "RGB(255,0,0) 80");
}
internal static void WriteUserSettingsString(string key, string propertyName,
string value)
{
var store = GuidesSettingsManager
.VsManagedSettingsManager
.GetWritableSettingsStore(SettingsScope.UserSettings);
store.CreateCollection(key);
store.SetString(key, propertyName, value);
}
// Persists settings and sets property with side effect of signaling
// ColumnGuideAdornments to update.
static private void WriteSettings(Color color, IEnumerable<int> columns)
{
string value = ComposeSettingsString(color, columns);
GuidelinesConfiguration = value;
}
private static string ComposeSettingsString(Color color,
IEnumerable<int> columns)
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat("RGB({0},{1},{2})", color.R, color.G, color.B);
IEnumerator<int> columnsEnumerator = columns.GetEnumerator();
if (columnsEnumerator.MoveNext())
{
sb.AppendFormat(" {0}", columnsEnumerator.Current);
while (columnsEnumerator.MoveNext())
{
sb.AppendFormat(", {0}", columnsEnumerator.Current);
}
}
return sb.ToString();
}
// Parse a color out of a string that begins like "RGB(255,0,0)"
static internal Color GuidelinesColor
{
get
{
string config = GuidelinesConfiguration;
if (!String.IsNullOrEmpty(config) && config.StartsWith("RGB("))
{
int lastParen = config.IndexOf(')');
if (lastParen > 4)
{
string[] rgbs = config.Substring(4, lastParen - 4).Split(',');
if (rgbs.Length >= 3)
{
byte r, g, b;
if (byte.TryParse(rgbs[0], out r) &&
byte.TryParse(rgbs[1], out g) &&
byte.TryParse(rgbs[2], out b))
{
return Color.FromRgb(r, g, b);
}
}
}
}
return Colors.DarkRed;
}
set
{
WriteSettings(value, GetColumnOffsets());
}
}
// Parse a list of integer values out of a string that looks like
// "RGB(255,0,0) 1, 5, 10, 80"
static internal List<int> GetColumnOffsets()
{
var result = new List<int>();
string settings = GuidesSettingsManager.GuidelinesConfiguration;
if (String.IsNullOrEmpty(settings))
return new List<int>();
if (!settings.StartsWith("RGB("))
return new List<int>();
int lastParen = settings.IndexOf(')');
if (lastParen <= 4)
return new List<int>();
string[] columns = settings.Substring(lastParen + 1).Split(',');
int columnCount = 0;
foreach (string columnText in columns)
{
int column = -1;
// VS 2008 gallery extension didn't allow zero, so per user request ...
if (int.TryParse(columnText, out column) && column >= 0)
{
columnCount++;
result.Add(column);
if (columnCount >= _maxGuides)
break;
}
}
return result;
}
// Delegate and Event to fire when settings change so that ColumnGuideAdornments
// can update. We need nothing special in this event since the settings manager
// is statically available.
//
internal delegate void SettingsChangedHandler();
static internal event SettingsChangedHandler SettingsChanged;
}
}
Most of this code creates and parses the settings format: "RGB(<int>,<int>,<int>) <int>, <int>, ...". The integers at the end are the one-based columns where you want column guides. The column guides extension captures all its settings in a single setting value string.
There are some parts of the code worth highlighting. The following line of code gets the Visual Studio managed wrapper for the settings storage. For the most part, this abstracts over the Windows registry, but this API is independent of the storage mechanism.
internal static SettingsManager VsManagedSettingsManager =
new ShellSettingsManager(ServiceProvider.GlobalProvider);
The Visual Studio settings storage uses a category identifier and a setting identifier to uniquely identify all settings:
private const string _collectionSettingsName = "Text Editor";
private const string _settingName = "Guides";
You do not have to use "Text Editor"
as the category name. You can pick anything you like.
The first few functions are the entry points to change settings. They check high-level constraints like maximum number of guides allowed. Then, they call WriteSettings
, which composes a settings string and sets the property GuideLinesConfiguration
. Setting this property saves the settings value to the Visual Studio settings store and fires the SettingsChanged
event to update all the ColumnGuideAdornment
objects, each associated with a text view.
There are a couple of entry point functions, such as CanAddGuideline
, which are used to implement commands that change settings. When Visual Studio shows menus, it queries command implementations to see if the command is currently enabled, what its name is, and so on. Below you see how to hook up these entry points for the command implementations. For more information on commands, see Extend menus and commands.
Implement the ColumnGuideAdornment class
The ColumnGuideAdornment
class is instantiated for each text view that can have adornments. This class listens for events about the view changing or settings changing, and the updating or redrawing column guides as necessary.
Replace the contents of the ColumnGuideAdornment.cs with the following code (explained below):
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);
}
}
}
Instances of this class hold onto the associated IWpfTextView and a list of Line
objects drawn on the view.
The constructor (called from ColumnGuideAdornmentTextViewCreationListener
when Visual Studio creates new views) creates the column guide Line
objects. The constructor also adds handlers for the SettingsChanged
event (defined in GuidesSettingsManager
) and the view events LayoutChanged
and Closed
.
The LayoutChanged
event fires due to several kinds of changes in the view, including when Visual Studio creates the view. The OnViewLayoutChanged
handler calls AddGuidelinesToAdornmentLayer
to execute. The code in OnViewLayoutChanged
determines if it needs to update line positions based on changes such as font size changes, view gutters, horizontal scrolling, and so on. The code in UpdatePositions
causes guide lines to draw between characters or just after the column of text that is in the specified character offset in the line of text.
Whenever settings change the SettingsChanged
function just recreates all the Line
objects with whatever the new settings are. After setting the line positions, the code removes all previous Line
objects from the ColumnGuideAdornment
adornment layer and adds the new ones.
Define the commands, menus, and menu placements
There can be a lot to declaring commands and menus, placing groups of commands or menus on various other menus, and hooking up command handlers. This walkthrough highlights how commands work in this extension, but for deeper information, see Extend menus and commands.
Introduction to the code
The Column Guides extension shows declaring a group of commands that belong together (add column, remove column, change line color), and then placing that group on a sub menu of the editor's context menu. The Column Guides extension also adds the commands to the main Edit menu but keeps them invisible, discussed as a common pattern below.
There are three parts to the commands implementation: ColumnGuideCommandsPackage.cs, ColumnGuideCommandsPackage.vsct, and ColumnGuideCommands.cs. The code generated by the templates puts a command on the Tools menu that pops a dialog box as the implementation. You can look at how that is implemented in the .vsct and ColumnGuideCommands.cs files since it is straightforward. You replace the code in these files below.
The package code contains boilerplate declarations required for Visual Studio to discover that the extension offers commands and to find where to place the commands. When the package initializes, it instantiates the commands implementation class. For more information about packages relating to commands, see Extend menus and commands.
A common commands pattern
The commands in the Column Guides extension are an example of a very common pattern in Visual Studio. You put related commands in a group, and you put that group on a main menu, often with "<CommandFlag>CommandWellOnly</CommandFlag>
" set to make the command invisible. Putting commands on the main menus (such as Edit) gives them nice names (such as Edit.AddColumnGuide), which are useful for finding commands when re-assigning key bindings in Tools Options. It's also useful for getting completion when invoking commands from the Command Window.
You then add the group of commands to context menus or sub menus where you expect user to use the commands. Visual Studio treats CommandWellOnly
as an invisibility flag for main menus only. When you place the same group of commands on a context menu or sub menu, the commands are visible.
As part of the common pattern, the Column Guides extension creates a second group that holds a single sub menu. The sub menu in turn contains the first group with the four-column guide commands. The second group that holds the sub menu is the reusable asset that you place on various context menus, which puts a sub menu on those context menus.
The .vsct file
The .vsct file declares the commands and where they go, along with icons and so on. Replace the contents of the .vsct file with the following code (explained below):
<?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. For Visual Studio to find your command handlers and invoke them, you need to ensure the package GUID declared in the ColumnGuideCommandsPackage.cs file (generated from the project item template) matches the package GUID declared in the .vsct file (copied from above). If you re-use this sample code, you should make sure you have a different GUID so that you do not conflict with anyone else who may have copied this code.
Find this line in ColumnGuideCommandsPackage.cs and copy the GUID from between the quotation marks:
public const string PackageGuidString = "ef726849-5447-4f73-8de5-01b9e930f7cd";
Then, paste the GUID in the .vsct file so that you have the following line in your Symbols
declarations:
<GuidSymbol name="guidColumnGuideCommandsPkg"
value="{ef726849-5447-4f73-8de5-01b9e930f7cd}" />
The GUIDs for the command set and the bitmap image file should be unique for your extensions, too:
<GuidSymbol name="guidColumnGuidesCommandSet"
value="{c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e}">
<GuidSymbol name="guidImages" value="{2C99F852-587C-43AF-AA2D-F605DE2E46EF}">
But, you do not need to change the command set and bitmap image GUIDs in this walkthrough to get the code to work. The command set GUID needs to match the declaration in the ColumnGuideCommands.cs file, but you replace the contents of that file, too; therefore, the GUIDs will match.
Other GUIDs in the .vsct file identify pre-existing menus to which the column guide commands are added, so they never change.
File sections. The .vsct has three outer sections: commands, placements, and symbols. The commands section defines command groups, menus, buttons or menu items, and bitmaps for icons. The placements section declares where groups go on menus or additional placements onto pre-existing menus. The symbols section declares identifiers used elsewhere in the .vsct file, which makes the .vsct code more readable than having GUIDs and hex numbers everywhere.
Commands section, groups definitions. The commands section first defines command groups. Groups of commands are commands you see in menus with slight gray lines separating the groups. A group may also fill an entire sub menu, as in this example, and you do not see the gray separating lines in this case. The .vsct files declare two groups, the GuidesMenuItemsGroup
that is parented to the IDM_VS_MENU_EDIT
(the main Edit menu) and the GuidesContextMenuGroup
that is parented to the IDM_VS_CTXT_CODEWIN
(the code editor's context menu).
The second group declaration has a 0x0600
priority:
<Group guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x0600">
The idea is to put the column guides sub menu at the end of any context menu to which you add the sub menu group. But, you shouldn't assume you know best and force the sub menu to always be last by using a priority of 0xFFFF
. You have to experiment with the number to see where your sub menu lies on the context menus where you place it. In this case, 0x0600
is high enough to put it at the end of the menus as far as you can see, but it leaves room for someone else to design their extension to be lower than the column guides extension if that's desirable.
Commands section, menu definition. Next, the command section defines the sub menu GuidesSubMenu
, parented to the GuidesContextMenuGroup
. The GuidesContextMenuGroup
is the group you add to all the relevant context menus. In the placements section, the code places the group with the four-column guide commands on this sub menu.
Commands section, buttons definitions. The commands section then defines the menu items or buttons that are the four-column guides commands. CommandWellOnly
, discussed above, means the commands are invisible when placed on a main menu. Two of the menu item button declarations (add guide and remove guide) also have an AllowParams
flag:
<CommandFlag>AllowParams</CommandFlag>
This flag enables, along with having main menu placements, the command to receive arguments when Visual Studio invokes the command handler. If the user runs the command from the Command Window, the argument is passed to the command handler in the event arguments.
Command sections, bitmaps definitions. Lastly, the commands section declares the bitmaps or icons used for the commands. This section is a simple declaration that identifies the project resource and lists one-based indexes of used icons. The symbols section of the .vsct file declares the values of the identifiers used as indexes. This walkthrough uses the bitmap strip provided with the custom command item template added to the project.
Placements section. After the commands section is the placements section. The first one is where the code adds the first group discussed above that holds the four-column guide commands to the sub menu where the commands appear:
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0x0100">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" />
</CommandPlacement>
All of the other placements add the GuidesContextMenuGroup
(which contains the GuidesSubMenu
) to other editor context menus. When the code declared the GuidesContextMenuGroup
, it was parented to the code editor's context menu. That's why you don't see a placement for the code editor's context menu.
Symbols section. As stated above, the symbols section declares identifiers used elsewhere in the .vsct file, which makes the .vsct code more readable than having GUIDs and hex numbers everywhere. The important points in this section are that the package GUID must agree with the declaration in the package class. And, the command set GUID must agree with the declaration in the command implementation class.
Implement the commands
The ColumnGuideCommands.cs file implements the commands and hooks up the handlers. When Visual Studio loads the package and initializes it, the package in turn calls Initialize
on the commands implementation class. The commands initialization simply instantiates the class, and the constructor hooks up all the command handlers.
Replace the contents of the ColumnGuideCommands.cs file with the following code (explained below):
using System;
using System.ComponentModel.Design;
using System.Globalization;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio;
namespace ColumnGuides
{
/// <summary>
/// Command handler
/// </summary>
internal sealed class ColumnGuideCommands
{
const int cmdidAddColumnGuide = 0x0100;
const int cmdidRemoveColumnGuide = 0x0101;
const int cmdidChooseGuideColor = 0x0102;
const int cmdidRemoveAllColumnGuides = 0x0103;
/// <summary>
/// Command menu group (command set GUID).
/// </summary>
static readonly Guid CommandSet =
new Guid("c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e");
/// <summary>
/// VS Package that provides this command, not null.
/// </summary>
private readonly Package package;
OleMenuCommand _addGuidelineCommand;
OleMenuCommand _removeGuidelineCommand;
/// <summary>
/// Initializes the singleton instance of the command.
/// </summary>
/// <param name="package">Owner package, not null.</param>
public static void Initialize(Package package)
{
Instance = new ColumnGuideCommands(package);
}
/// <summary>
/// Gets the instance of the command.
/// </summary>
public static ColumnGuideCommands Instance
{
get;
private set;
}
/// <summary>
/// Initializes a new instance of the <see cref="ColumnGuideCommands"/> class.
/// Adds our command handlers for menu (commands must exist in the command
/// table file)
/// </summary>
/// <param name="package">Owner package, not null.</param>
private ColumnGuideCommands(Package package)
{
if (package == null)
{
throw new ArgumentNullException("package");
}
this.package = package;
// Add our command handlers for menu (commands must exist in the .vsct file)
OleMenuCommandService commandService =
this.ServiceProvider.GetService(typeof(IMenuCommandService))
as OleMenuCommandService;
if (commandService != null)
{
// Add guide
_addGuidelineCommand =
new OleMenuCommand(AddColumnGuideExecuted, null,
AddColumnGuideBeforeQueryStatus,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidAddColumnGuide));
_addGuidelineCommand.ParametersDescription = "<column>";
commandService.AddCommand(_addGuidelineCommand);
// Remove guide
_removeGuidelineCommand =
new OleMenuCommand(RemoveColumnGuideExecuted, null,
RemoveColumnGuideBeforeQueryStatus,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidRemoveColumnGuide));
_removeGuidelineCommand.ParametersDescription = "<column>";
commandService.AddCommand(_removeGuidelineCommand);
// Choose color
commandService.AddCommand(
new MenuCommand(ChooseGuideColorExecuted,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidChooseGuideColor)));
// Remove all
commandService.AddCommand(
new MenuCommand(RemoveAllGuidelinesExecuted,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidRemoveAllColumnGuides)));
}
}
/// <summary>
/// Gets the service provider from the owner package.
/// </summary>
private IServiceProvider ServiceProvider
{
get
{
return this.package;
}
}
private void AddColumnGuideBeforeQueryStatus(object sender, EventArgs e)
{
int currentColumn = GetCurrentEditorColumn();
_addGuidelineCommand.Enabled =
GuidesSettingsManager.CanAddGuideline(currentColumn);
}
private void RemoveColumnGuideBeforeQueryStatus(object sender, EventArgs e)
{
int currentColumn = GetCurrentEditorColumn();
_removeGuidelineCommand.Enabled =
GuidesSettingsManager.CanRemoveGuideline(currentColumn);
}
private int GetCurrentEditorColumn()
{
IVsTextView view = GetActiveTextView();
if (view == null)
{
return -1;
}
try
{
IWpfTextView textView = GetTextViewFromVsTextView(view);
int column = GetCaretColumn(textView);
// Note: GetCaretColumn returns 0-based positions. Guidelines are 1-based
// positions.
// However, do not subtract one here since the caret is positioned to the
// left of
// the given column and the guidelines are positioned to the right. We
// want the
// guideline to line up with the current caret position. e.g. When the
// caret is
// at position 1 (zero-based), the status bar says column 2. We want to
// add a
// guideline for column 1 since that will place the guideline where the
// caret is.
return column;
}
catch (InvalidOperationException)
{
return -1;
}
}
/// <summary>
/// Find the active text view (if any) in the active document.
/// </summary>
/// <returns>The IVsTextView of the active view, or null if there is no active
/// document or the
/// active view in the active document is not a text view.</returns>
private IVsTextView GetActiveTextView()
{
IVsMonitorSelection selection =
this.ServiceProvider.GetService(typeof(IVsMonitorSelection))
as IVsMonitorSelection;
object frameObj = null;
ErrorHandler.ThrowOnFailure(
selection.GetCurrentElementValue(
(uint)VSConstants.VSSELELEMID.SEID_DocumentFrame, out frameObj));
IVsWindowFrame frame = frameObj as IVsWindowFrame;
if (frame == null)
{
return null;
}
return GetActiveView(frame);
}
private static IVsTextView GetActiveView(IVsWindowFrame windowFrame)
{
if (windowFrame == null)
{
throw new ArgumentException("windowFrame");
}
object pvar;
ErrorHandler.ThrowOnFailure(
windowFrame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView, out pvar));
IVsTextView textView = pvar as IVsTextView;
if (textView == null)
{
IVsCodeWindow codeWin = pvar as IVsCodeWindow;
if (codeWin != null)
{
ErrorHandler.ThrowOnFailure(codeWin.GetLastActiveView(out textView));
}
}
return textView;
}
private static IWpfTextView GetTextViewFromVsTextView(IVsTextView view)
{
if (view == null)
{
throw new ArgumentNullException("view");
}
IVsUserData userData = view as IVsUserData;
if (userData == null)
{
throw new InvalidOperationException();
}
object objTextViewHost;
if (VSConstants.S_OK
!= userData.GetData(Microsoft.VisualStudio
.Editor
.DefGuidList.guidIWpfTextViewHost,
out objTextViewHost))
{
throw new InvalidOperationException();
}
IWpfTextViewHost textViewHost = objTextViewHost as IWpfTextViewHost;
if (textViewHost == null)
{
throw new InvalidOperationException();
}
return textViewHost.TextView;
}
/// <summary>
/// Given an IWpfTextView, find the position of the caret and report its column
/// number. The column number is 0-based
/// </summary>
/// <param name="textView">The text view containing the caret</param>
/// <returns>The column number of the caret's position. When the caret is at the
/// leftmost column, the return value is zero.</returns>
private static int GetCaretColumn(IWpfTextView textView)
{
// This is the code the editor uses to populate the status bar.
Microsoft.VisualStudio.Text.Formatting.ITextViewLine caretViewLine =
textView.Caret.ContainingTextViewLine;
double columnWidth = textView.FormattedLineSource.ColumnWidth;
return (int)(Math.Round((textView.Caret.Left - caretViewLine.Left)
/ columnWidth));
}
/// <summary>
/// Determine the applicable column number for an add or remove command.
/// The column is parsed from command arguments, if present. Otherwise
/// the current position of the caret is used to determine the column.
/// </summary>
/// <param name="e">Event args passed to the command handler.</param>
/// <returns>The column number. May be negative to indicate the column number is
/// unavailable.</returns>
/// <exception cref="ArgumentException">The column number parsed from event args
/// was not a valid integer.</exception>
private int GetApplicableColumn(EventArgs e)
{
var inValue = ((OleMenuCmdEventArgs)e).InValue as string;
if (!string.IsNullOrEmpty(inValue))
{
int column;
if (!int.TryParse(inValue, out column) || column < 0)
throw new ArgumentException("Invalid column");
return column;
}
return GetCurrentEditorColumn();
}
/// <summary>
/// This function is the callback used to execute a command when the a menu item
/// is clicked. See the Initialize method to see how the menu item is associated
/// to this function using the OleMenuCommandService service and the MenuCommand
/// class.
/// </summary>
private void AddColumnGuideExecuted(object sender, EventArgs e)
{
int column = GetApplicableColumn(e);
if (column >= 0)
{
GuidesSettingsManager.AddGuideline(column);
}
}
private void RemoveColumnGuideExecuted(object sender, EventArgs e)
{
int column = GetApplicableColumn(e);
if (column >= 0)
{
GuidesSettingsManager.RemoveGuideline(column);
}
}
private void RemoveAllGuidelinesExecuted(object sender, EventArgs e)
{
GuidesSettingsManager.RemoveAllGuidelines();
}
private void ChooseGuideColorExecuted(object sender, EventArgs e)
{
System.Windows.Media.Color color = GuidesSettingsManager.GuidelinesColor;
using (System.Windows.Forms.ColorDialog picker =
new System.Windows.Forms.ColorDialog())
{
picker.Color = System.Drawing.Color.FromArgb(255, color.R, color.G,
color.B);
if (picker.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
GuidesSettingsManager.GuidelinesColor =
System.Windows.Media.Color.FromRgb(picker.Color.R,
picker.Color.G,
picker.Color.B);
}
}
}
}
}
Fix references. You're missing a reference at this point. Press the right pointer button on the References node in the Solution Explorer. Choose the Add ... command. The Add Reference dialog has a search box in the upper-right corner. Enter "editor" (without the double quotes). Choose the Microsoft.VisualStudio.Editor item (you must check the box to the left of the item, not just select the item) and choose OK to add the reference.
Initialization. When the package class initializes, it calls Initialize
on the commands implementation class. The ColumnGuideCommands
initialization instantiates the class and saves the class instance and the package reference in class members.
Let's look at one of the command handler hook-ups from the class constructor:
_addGuidelineCommand =
new OleMenuCommand(AddColumnGuideExecuted, null,
AddColumnGuideBeforeQueryStatus,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidAddColumnGuide));
You create an OleMenuCommand
. Visual Studio uses the Microsoft Office command system. The key arguments when instantiating an OleMenuCommand
is the function that implements the command (AddColumnGuideExecuted
), the function to call when Visual Studio shows a menu with the command (AddColumnGuideBeforeQueryStatus
), and the command ID. Visual studio calls the query status function before showing a command on a menu so that the command can make itself invisible or greyed out for a particular display of the menu (for example, disabling Copy if there's no selection), change its icon, or even change its name (for example, from Add Something to Remove Something), and so on. The command ID must match a command ID declared in the .vsct file. The strings for the command set and the column guides add command must match between the .vsct file and the ColumnGuideCommands.cs.
The following line provides assistance for when users invoke the command via the Command Window (explained below):
_addGuidelineCommand.ParametersDescription = "<column>";
Query status. The query status functions AddColumnGuideBeforeQueryStatus
and RemoveColumnGuideBeforeQueryStatus
check some settings (such as max number of guides or max column) or if there is a column guide to remove. They enable the commands if the conditions are right. Query status functions need to be efficient because they run every time Visual Studio shows a menu and for each command on the menu.
AddColumnGuideExecuted function. The interesting part of adding a guide is figuring out the current editor view and caret location. First, this function calls GetApplicableColumn
, which checks if there is a user-supplied argument in the command handler's event arguments, and if there is none, the function checks the editor's view:
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
has to dig a little to get an IWpfTextView view of the code. If you trace through GetActiveTextView
, GetActiveView
, and GetTextViewFromVsTextView
, you can see how to do that. The following code is the relevant code abstracted, starting with the current selection, then getting the selection's frame, then getting the frame's DocView as an IVsTextView, then getting an IVsUserData from the IVsTextView, then getting a view host, and finally the 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;
Once you have an IWpfTextView, you can get the column where the caret is located:
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));
}
With the current column in hand where the user clicked, the code just calls on the settings manager to add or remove the column. The settings manager fires the event to which all ColumnGuideAdornment
objects listen. When the event fires, these objects update their associated text views with new column guide settings.
Invoke command from the Command Window
The column guides sample enables users to invoke two commands from the Command Window as a form of extensibility. If you use the View | Other Windows | Command Window command, you can see the Command Window. You can interact with the Command Window by entering "edit.", and with command name completion and supplying the argument 120, you have the following result:
> Edit.AddColumnGuide 120
>
The pieces of the sample that enable this behavior are in the .vsct file declarations, the ColumnGuideCommands
class constructor when it hooks up command handlers, and the command handler implementations that check event arguments.
You saw "<CommandFlag>CommandWellOnly</CommandFlag>
" in the .vsct file as well as placements in the Edit main menu even though the commands are not shown in the Edit menu UI. Having them on the main Edit menu gives them names like Edit.AddColumnGuide. The commands group declaration that holds the four commands placed the group on the Edit menu directly:
<Group guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0xB801">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_EDIT" />
</Group>
The buttons section later declared the commands CommandWellOnly
to keep them invisible on the main menu and declared them with 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>
You saw the command handler hook up code in the ColumnGuideCommands
class constructor provided a description of the allowed parameter:
_addGuidelineCommand.ParametersDescription = "<column>";
You saw the GetApplicableColumn
function checks OleMenuCmdEventArgs
for a value before checking the editor's view for a current column:
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;
}
Try your extension
You can now press F5 to execute your Column Guides extension. Open a text file and use the editor's context menu to add guide lines, remove them, and change their color. Click in the text (not whitespace passed the end of the line) to add a column guide, or the editor adds it to the last column on the line. If you use the Command Window and invoke the commands with an argument, you can add column guides anywhere.
If you want to try different command placements, change names, change icons, and so on, and you have any problems with Visual Studio showing you the latest code in menus, you can reset the experimental hive in which you are debugging. Bring up the Windows Start Menu and type "reset". Look for and run the command, Reset the Next Visual Studio Experimental Instance. This command cleans up the experimental registry hive of all extension components. It does not clean out settings from components, so any guides you had when you shut down Visual Studio's experimental hive are still there when your code reads the settings store on next launch.
Finished code project
There will soon be a GitHub project of Visual Studio Extensibility samples, and the completed project will be there. This article will be updated to point there when that happens. The completed sample project may have different guids and will have a different bitmaps strip for the command icons.
You can try out a version of the column guides feature with this Visual Studio Galleryextension.