Adventures in Visual Studio Extension Development: Part 1 - The Basics
This is part of a series:
- Part 1: The Basics
- Part 2: Events
One of the major annoyances with Visual Studio Extension development is finding out how to make them. It is relatively simple to use the extension wizard and get a command in the Tools
menu. At that point you really have no idea why or how it works. Doing anything beyond that simple exercise takes a leap in understanding which is not easy to come by. You search and read and follow the rabbit hole. Just when you think you won't figure it out at all, you'll find that one answer that opens up a whole new realm of possibilities. But maybe that is just me.
I am starting this series of posts in case it isn't just me. I don't have a specific number of posts I'm planning to do. At this point I know that I will cover the first public release of NArrangeVS in this post and then over the next week I will be making some enhancements and writing about the things I have figured out. After that it will probably be any time I make a major enhancement to the project.
Getting Started
The first step is to download the Microsoft Visual Studio 2013 SDK and install it. Once installed, there are new project templates under the Extensibility heading. I started with a Visual Studio Package as show below (actually I started with a VSIX Project the first time and found out it starts with a blank project).
This project type goes through the Visual Studio Package Wizard. The options are pretty self explanatory. For this project I started with just a Menu Command as shown below. After selecting a Menu Command, it asks for the name and id of the command, which we will revisit in a minute. The last step will help with creating test projects.
Anatomy of a Visual Studio Package
VSPackage
The core of the package is a class which inherits from Microsoft.VisualStudio.Shell.Package
. The wizard set up the plumbing for our menu command. One of the first things to notice, is that what the package provides is specified with attributes. Here are the ones set by the wizard.
[PackageRegistration(UseManagedResourcesOnly = true)]
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid(GuidList.guidNArrangeVSPkgString)]
The PackageRegistrationAttribute
is the magic that makes Visual Studio register our VSPackage. The UseManagedResourcesOnly
option is set to true
by the wizard.
The InstalledProductRegistrationAttribute
is used to set the text for the Help / About Microsoft Visual Studio window. The first three strings set the package name, package description, and product id (version). The #110
and #112
refer to strings in the VSPackage.resx
file. The last thing that is set by the wizard is the IconResourceID
which is an icon set in the VSPackage.resx
file. It defaults to the Resources/Package.ico
file.
The ProvideMenuResourceAttribute
is one of many provide attributes in the Microsoft.VisualStudio.Shell
namespace. They tell Visual Studio what and where to look for extensions to the interface. In this case, we are providing a menu resource. So for some reason, Menus.ctmenu
refers to our NArrangeVS.vsct
file. You can see this by looking at the resource name for that file in the .csproj
file. The next parameter is a version number which can stay at 1 as far as I can tell.
The GuidAttribute
provides an explicit Guid for the class. It uses the package Guid defined in the Guids.cs
file.
vsixmanifest
There is a designer for this file to set values for the generated VSIX file. The Guid in this file matches the one specified for the VSPackage class. If you will be releasing your extension to the public, it helps to include as much metadata as possible. The assets section was already set up to load the VSPackage. The install target defaults to Visual Studio 12.0.
vsct
The vsct file is where things get really crazy. It is an XML file that defines where and how to place our commands in the Visual Studio interface. I will be diving into this file in a future article, so I won't spend too much time going over it. I will just do the basics that were automatically set by the wizard. Below is the source code with the comments removed. The comments are actually not too bad; take some time to read them if you want to know more.
<?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">
<Extern href="stdidcmd.h"/>
<Extern href="vsshlids.h"/>
<Commands package="guidNArrangeVSPkg">
<Groups>
<Group guid="guidNArrangeVSCmdSet" id="MyMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
</Group>
</Groups>
<Buttons>
<Button guid="guidNArrangeVSCmdSet" id="cmdidArrangeFile" priority="0x0100" type="Button">
<Parent guid="guidNArrangeVSCmdSet" id="MyMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>ArrangeFile</ButtonText>
</Strings>
</Button>
</Buttons>
<Bitmaps>
<Bitmap guid="guidImages" href="Resources\Images.png" usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows"/>
</Bitmaps>
</Commands>
<Symbols>
<GuidSymbol name="guidNArrangeVSPkg" value="{ec9e3f3e-c4a2-4e6b-b11b-2220397f3f08}" />
<GuidSymbol name="guidNArrangeVSCmdSet" value="{2613e1f4-5093-46c9-8038-8ded6e2dffac}">
<IDSymbol name="MyMenuGroup" value="0x1020" />
<IDSymbol name="cmdidArrangeFile" value="0x0100" />
</GuidSymbol>
<GuidSymbol name="guidImages" value="{c2645351-7b0e-46a8-98ee-e906d4c27321}" >
<IDSymbol name="bmpPic1" value="1" />
<IDSymbol name="bmpPic2" value="2" />
<IDSymbol name="bmpPicSearch" value="3" />
<IDSymbol name="bmpPicX" value="4" />
<IDSymbol name="bmpPicArrows" value="5" />
<IDSymbol name="bmpPicStrikethrough" value="6" />
</GuidSymbol>
</Symbols>
</CommandTable>
The first two Extern
elements load a whole bunch of defines for command ID and GUIDs. Let's jump to the Symbols
section next. It defines IDs and GUIDs for our extension. The package and command set GUIDs need to match the values in the Guids.cs
file. The command ID needs to match the one defined in the PkgCmdID.cs
file. The guidImages defines values which are used by the Commands/Bitmaps
section and for Icon
elements.
The Commands
section is where our commands are actually defined. The Groups
section is where new menu groups are defined. The wizard automatically created a new group in the Tools
menu (using the guidSHLMainMenu
and IDM_VS_MENU_TOOLS
values which are set in vsshlids.h
). The Buttons
section adds the actual commands to the menus. The wizard automatically added our one tool to the group which was created. It also sets the icon and text to use for the command. The Bitmaps
section defines bitmaps to use for the icons. One image can be setup to provide icons for multiple commands which is what the wizard did automatically. Open the Resources/Images.png
file to edit the default command icon. It will be the first section of the image.
Menu Command with Options
The wizard created a menu command which just shows a dialog box saying that we are inside the callback method. I'm going to modify this method to make it a little more interesting. To do that, I will also be adding an options page to allow the user to set some values. The first step is to create a class which inherits from DialogPage
.
[ClassInterface(ClassInterfaceType.AutoDual)]
[CLSCompliant(false)]
[ComVisible(true)]
public class NArrangeVSOptions : DialogPage
{
private static readonly string DefaultNArrangeFolderPath = @"C:\Program Files (x86)\NArrange 0.2.9";
[Category("NArrange Information")]
[DisplayName("NArrange Config Location")]
[Description("The location of the NArrange XML configuration file to be used for arranging the files.")]
public string NArrangeConfigLocation
{
get;
set;
}
[Category("NArrange Information")]
[DisplayName("NArrange Console Location")]
[Description("The location of the narrange-console.exe file.")]
public string NArrangeConsoleLocation
{
get;
set;
}
public override void LoadSettingsFromStorage()
{
NArrangeConsoleLocation = Path.Combine(DefaultNArrangeFolderPath, "narrange-console.exe");
NArrangeConfigLocation = Path.Combine(DefaultNArrangeFolderPath, "DefaultConfig.xml");
base.LoadSettingsFromStorage();
}
}
In this class, I defined two properties and use the CategoryAttribute
, DisplayNameAttribute
, and DescriptionAttribute
to set the category, name, and description for the setting respectively. On the class, we set up some COM and CLR options using the ClassInterfaceAttribute
, CLSCompliantAttribute
, and ComVisibleAttribute
. Then we override the LoadSettingsFromStorage
method to set the default values for our properties before calling the base to load the settings from storage if they have been changed from the defaults.
Creating the DialogPage
class isn't enough to get the options into the Tools/Options
window. We have to use one of the provide attributes which were discussed earlier. In this ccase we need to add the ProvideOptionPageAttribute
to the VSPackage.
[ProvideOptionPage(typeof(NArrangeVSOptions), "NArrangeVS", "General", 0, 0, true, new[] { "NArrange", })]
The first argument is a reference to our DialogPage
type. The second argument sets the name of the section in the options window. The third argument sets the subsection in the options window. The next two arguments set the resource IDs for the section and subsection name. The fifth argument sets whether other extensions can access these options. The last argument sets keywords for searching for the option page. With this in place, the following option page becomes available.
To actually do some code arranging, the following is implemented in the VSPackage class.
private readonly Lazy<DTE2> dte;
private readonly Lazy<NArrangeVSOptions> options;
private readonly Lazy<RunningDocumentTable> rdt;
private readonly Lazy<Microsoft.VisualStudio.OLE.Interop.IServiceProvider> sp;
public NArrangeVSPackage()
{
options = new Lazy<NArrangeVSOptions>(() => GetDialogPage(typeof(NArrangeVSOptions)) as NArrangeVSOptions, true);
dte = new Lazy<DTE2>(() => ServiceProvider.GlobalProvider.GetService(typeof(EnvDTE.DTE)) as DTE2);
sp = new Lazy<Microsoft.VisualStudio.OLE.Interop.IServiceProvider>(() => Package.GetGlobalService(typeof(Microsoft.VisualStudio.OLE.Interop.IServiceProvider)) as Microsoft.VisualStudio.OLE.Interop.IServiceProvider);
rdt = new Lazy<RunningDocumentTable>(() => new RunningDocumentTable(new ServiceProvider(sp.Value)));
}
protected override void Initialize()
{
Debug.WriteLine(string.Format(CultureInfo.CurrentCulture, "Entering Initialize() of: {0}", this.ToString()));
base.Initialize();
// Add our command handlers for menu (commands must exist in the .vsct file)
OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
if (null != mcs) {
// Create the command for the menu item.
CommandID menuCommandID = new CommandID(GuidList.guidNArrangeVSCmdSet, (int)PkgCmdIDList.cmdidArrangeFile);
MenuCommand menuItem = new MenuCommand(MenuItemCallback, menuCommandID);
mcs.AddCommand(menuItem);
}
}
private void MenuItemCallback(object sender, EventArgs e)
{
NArrangeDocument(dte.Value.ActiveDocument);
}
private void NArrangeDocument(EnvDTE.Document document)
{
if (document.ReadOnly) {
return;
}
dte.Value.UndoContext.Open("NArrangeVS");
ProcessStartInfo startInfo =
new ProcessStartInfo(
options.Value.NArrangeConsoleLocation,
string.Format("\"{0}\" /c:\"{1}\"", document.FullName, options.Value.NArrangeConfigLocation)
)
{
CreateNoWindow = true,
ErrorDialog = false,
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
UseShellExecute = false,
WorkingDirectory = document.Path
};
Process process = Process.Start(startInfo);
while (!process.HasExited) {
Thread.Sleep(100);
}
IVsPersistDocData docData = rdt.Value.FindDocument(document.FullName) as IVsPersistDocData;
if (docData != null) {
docData.ReloadDocData((uint)_VSRELOADDOCDATA.RDD_IgnoreNextFileChange);
}
dte.Value.UndoContext.Close();
}
The implementation is not extremely complex. I use a local lazy reference to the DTE to open and close an UndoContext (I find out later that this can cause some issues). Then I call the NArrange console program for the file. I also found that in Visual Studio 2013, the document does not automatically reload in this scenario. It took a lot of searching to find the ReloadDocData
method and find out how to get it from the RunningDocumentTable
. I make some significant changes to this method in the next few releases and find out how to avoid doing this.
Next Time
In the next article, I will dive into using document events and fixing a crazy bug or two.