Joshua Thompson

Engineer · Programmer · Web Enthusiast

Adventures in Visual Studio Extension Development: Part 1 - The Basics

This is part of a series:

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

Creating a Visual Studio Package
Creating a Visual Studio Package

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.

Visual Studio Package Wizard - Choosing a Menu Command
Visual Studio Package Wizard - Choosing a Menu Command

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.

Options Page for Menu Command Only
Options Page for Menu Command Only

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.