Special connect() issue 2014

Volume 29 Number 12A

Visual Studio 2013 : Expand Visual Studio 2013 with Extensions

Doug Erickson; issue 2014

One of the bestfeatures in the higher-level versions of Visual Studio is now available to everybody for free through the Visual Studio Community 2013 IDE: the ability to use extensions from the Visual Studio Gallery (find the extensions at bit.ly/115mBzm). So what are extensions? They’re plug-ins that let you extend Visual Studio Community 2013 (which you can download at bit.ly/1tH2uyc) and features of Visual Studio to perform new tasks, add new features and refactor code—or even support new languages.

The Microsoft developer community provides a wide variety of extensions on the Visual Studio Gallery. Take a look and you’ll be surprised at the kinds of extensions available. Some of them, like the Productivity Power Tools 2013 extension (download at bit.ly/1xE3EAT), you may not want to live without. You can install these extensions from the Web, or you can search within Visual Studio for online extensions using the Extensions and Updates window in the Tools menu. Look at the Online category to find more popular tools like Visual Assist and ReSharper. You can even find more extensions at the Code Gallery (bit.ly/11nzi9Q).

Even the best tools may not map perfectly to specific tasks you want to perform or automate within the IDE. You probably have your own personal scripts you’ve customized and tweaked to make your work easier. Maybe you have one that checks a build directory for a successful build, or runs a transform on XML files or cleans up all the detritus of a complex build process. How would you like to run your tool within Visual Studio as part of the IDE or as part of your build process? Now you can, using Visual Studio 2013 Community and the Visual Studio 2013 SDK.

You can get started by downloading the Visual Studio SDK. It provides all the libraries, tools and project templates for creating a variety of different extensions. Go ahead and install it and you’ll be ready.

Set up Tools to Run in Visual Studio

Once you have the SDK installed, it’s pretty easy to make your tool or executable script run from a Visual Studio menu. The ubiquitous Notepad.exe will be the example for this article, but you can use any executable or integrate your own code in the command handler.

A basic extension is a Visual Studio Package binary. You can find the Visual Studio Package template in the New Project dialog under Visual Basic | Extensibility, C# | Extensibility, or Other Project Types | Extensibility.

This article will demonstrate how to make a simple extension that launches Notepad. We’ll use the template under C#/Extensibility to create a C# Visual Studio Package project. We’ll put it in the D:\Code directory and call it StartNotepad. This extension will ultimately start Notepad from a Visual Studio IDE menu item.

Once you double-click the template, Visual Studio launches a wizard to help you configure the Extension. For starters, just accept the defaults on the first two dialog pages.

Next, take the following steps:

  1. On the Select VSPackage Options page, select the Menu Command option.
  2. On the Command Options page, set the command name to Start Notepad and the command ID to cmdidStartNotepad.
  3. On the Test Options page, uncheck the two checkboxes.
  4. Click Finish.

At this point you can build and run the Package project. When the project opens, start debugging (press the F5 key, or use the Start command on the toolbar). A new instance of Visual Studio Community 2013 will start up. When that’s complete, you’ll see the IDE, with a default to Start Page – Microsoft Visual Studio – Experimental Instance on the title bar (see Figure 1). This is your test instance of Visual Studio. It runs separately from your working instance of Visual Studio, so you don’t have to worry about contaminating your own development environment if something goes wrong.

The Visual Studio Experimental Instance
Figure 1 The Visual Studio Experimental Instance

The “experimental instance” is really just a fancy way of announcing that you’ve launched a sandbox instance of Visual Studio for testing your extension. It has all the functions of Visual Studio Community 2013, but it doesn’t jeopardize your work in the original Visual Studio instance. For an example this simple, it may seem like overkill. However, if you build an entire design framework or a complex interdependent set of build tools, you don’t want to risk your code by running it in the same instance from which you’re developing.

On the Tools menu of the experimental instance, open the Extensions and Updates window. You should see the StartNotepad extension here (see Figure 2). (If you open Extensions and Updates in your working instance of Visual Studio, you won’t see StartNotePad.)

Extensions and Updates Showing StartNotepad
Figure 2 Extensions and Updates Showing StartNotepad

You’ll also see Start Notepad under the Tools menu (see Figure 3).

New Menu Item Confirmation for StartNotepad Under Tools
Figure 3 New Menu Item Confirmation for StartNotepad Under Tools

Now go to the Tools menu in the experimental instance. You should see the Start Notepad command. At this point it just brings up a message box that says “StartNotepad – Inside MSIT.StartNotepad.StartNotepadPackage.MenuItemCallback().” You’ll see how to actually start Notepad from this command in the next section.

Set up the Menu Command

Stop debugging and go back to your working instance of Visual Studio. Open the StartNotepadPackage.cs file, which contains the derived Package class. This is the starting point for all VSPackage extensions. The Initialize method of this class actually sets up the command:

// 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.guidStartNotepadCmdSet,
  MenuCommand menuItem = new MenuCommand(MenuItemCallback, menuCommandID );
  mcs.AddCommand( menuItem );

Don’t worry about the details of this code right now. You should note this is how the menu command is instantiated. The menus and other UI for VSPackage extensions are defined, as the code comment says, in the .vsct file.

The command handler is named MenuItemCallback (in the same StartNotepadPackage class). Delete the existing method and add the following:

private void MenuItemCallback(object sender, EventArgs e)
  Process proc = new Process();
  proc.StartInfo.FileName = "notepad.exe";

Now try it out. When you start debugging the project and click Tools | Start Notepad, you should see an instance of Notepad. In fact, you’ll get a new instance of Notepad every time you click Start Notepad. 

You can use an instance of the System.Diagnostics.Process class to run any executable, not just Notepad. Try it with calc.exe, for example.

Define the Interface

Now you’ll use the .vsct file to define the UI for your extension. Take a look at the StartNotepad.vsct file. This is a handy place to find all the definitions of the Visual Studio UI you’ll use in your extension. It’s an XML file, so brace yourself for angle braces.

Find the <Groups> block. Every menu command must belong to a Group, which tells Visual Studio where to put the command. In this case the command is on the Tools menu, and its parent is the Main menu:

  <Group guid="guidStartNotepadCmdSet" 
    id="MyMenuGroup" priority="0x0600">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>

Don’t worry too much about the details yet. This is just to show you where the important elements are. The command itself is defined within the <Buttons> block:

<Button guid="guidStartNotepadCmdSet" id="cmdidStartNotepad"
  priority="0x0100" type="Button">
  <Parent guid="guidStartNotepadCmdSet" id="MyMenuGroup" />
  <Icon guid="guidImages" id="bmpPic1" />
    <ButtonText>Start Notepad</ButtonText>

You can see the button is defined with a GUID (guidStartNotepadCmdSet, which is the same as the Group GUID) and an ID (cmdidStartNotepad, which is the ID you specified in the package wizard). The command is parented to the Group. It has an icon and some text. The icon is one of a default set of icons included in the solution. The priority specifies the location of this button on the menu, if there are multiple commands in the command group. The GUIDs and IDs for the groups and buttons are defined in the <Symbols> block:

<!-- This is the package GUID. -->
<GuidSymbol name="guidStartNotepadPkg"
  value="{18311db7-ca0f-419c-82b0-5aa14c8b541a}" />
<!-- This is the GUID used to group the menu commands together -->
<GuidSymbol name="guidStartNotepadCmdSet"
  <IDSymbol name="MyMenuGroup" value="0x1020" />
  <IDSymbol name="cmdidStartNotepad" value="0x0100" />

The bitmaps are also defined in the <Symbols> block:

<GuidSymbol name="guidImages" 
  value="{b8b810ad-5210-4f35-a491-c3464a612cb6}" >
  <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" />

Try changing the icon to one of the other ones defined here. None of the choices are particularly appropriate for Notepad, so choose the strikethrough because it shows another part of the setup. Although the icons are defined in the <Symbols> block, they must also be listed in the usedList attribute of the <Bitmaps> block:

<Bitmap guid="guidImages" href="Resources\Images.png" usedList="bmpPic1,
  bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows"/>

You see bmpPicStrikethrough isn’t listed, although it has been defined. If you change the icon to this bitmap, it won’t appear on the menu. So add bmpPicStrikethrough:

<Bitmap guid="guidImages" href="Resources\Images.png" usedList="bmpPic1,
  bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows, bmpPicStrikethrough"/>

Now you can change the icon to bmpPicStrikethrough for the menu button:

<Button guid="guidStartNotepadCmdSet" id="cmdidStartNotepad"
  priority="0x0100" type="Button">
  <Parent guid="guidStartNotepadCmdSet" id="MyMenuGroup" />
  <Icon guid="guidImages" id="bmpPicStrikethrough" />
    <ButtonText>Start Notepad</ButtonText>

Now you can try it out. When the experimental instance comes up, you should see something like Figure 4 on the Tools menu.

The Command to Start Notepad
Figure 4 The Command to Start Notepad

Now add a keyboard shortcut for the Start Notepad menu command. This example will use Ctrl+1. If you add a keyboard shortcut, be sure to pick an obscure key combination so your command doesn’t collide with a more standard combination. You can do this entirely in the .vsct file. Keyboard shortcuts are called KeyBindings in the .vsct file. Add the following block to the .vsct file:

  <KeyBinding guid="guidStartNotepadCmdSet" 
    editor="guidVSStd97" key1="1" mod1="CONTROL"/>

The key1 attribute declares the standard key and the mod1 attribute declares the modifier or accelerator key (typically Ctrl, Alt or Shift) pressed in conjunction with the standard key. If you add mod2="ALT" to add a second modifier key, you would have Ctrl+Alt+1 as the shortcut. Stick with Ctrl+1 for now. Keep in mind, though, the .vsct file is a good place to start your customizations. Now, you can start debugging. Relaunch the package. When the Experimental Instance comes up, hit Ctrl+1. You should get an instance of Notepad.

Keep the UI Responsive

The MenuItemCallback method as it currently exists doesn’t block the UI thread. You can still start Notepad with the Start Notepad command (or Ctrl+1). You can also still do things in the Visual Studio IDE. You can move the window around, click other commands on the menus and so on. Imagine your tool needs to complete its work before you exit the handler. In other words, you need to call Process.WaitForExit.

Unfortunately, if you call WaitForExit on the Visual Studio UI thread, as MenuItemCallback is doing at this point, the whole Visual Studio UI freezes. Check this out in action. In the MenuItemCallback method, call WaitForExit:

private void MenuItemCallback(object sender, EventArgs e)
  Process proc = new Process();
  proc.StartInfo.FileName = "notepad.exe";

Start debugging, and when the experimental instance comes up, hit Ctrl+1. You should see the instance of Notepad. Now, try moving the Visual Studio window (the experimental one). It won’t move. You can’t do anything else in the Visual Studio UI, either. You may even see the popup that says, “Microsoft Visual Studio is Busy.” Clearly, this is something you need to fix.

Fortunately, the fix is simple. If you have a tool or a process that will take a noticeable amount of time to complete, wrap it in a Task in MenuItemCallback, like so:

private void MenuItemCallback(object sender, EventArgs e)
  ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
    Process proc = new Process();
    proc.StartInfo.FileName = "notepad.exe";
    await proc;

Now you should be able to run your tool and work in Visual Studio at the same time.

Clean up Your Experiment

If you’re developing multiple extensions, or just exploring outcomes with different versions of your extension code, your experimental environment may stop working the way it should. In this case, you should run the reset script.

This script is called Reset the Visual Studio 2013 Experimental Instance and it ships as part of the Visual Studio 2013 SDK. This script removes all references to your extensions from the experimental environment so you can start from scratch. You can get to this script in one of two ways:

  • From the desktop, find Reset the Visual Studio 2013 Experimental Instance
  • From the command line, run the following:
<VSSDK installation>\VisualStudioIntegration\Tools\Bin\
        CreateExpInstance.exe /Reset /VSInstance=12.0 
       /RootSuffix=Exp && PAUSE

Deploy Your Extension

Now that your tool extension is running as it should, it’s time to think about sharing it with your friends and colleagues. That’s easy, as long as they have Visual Studio 2013 installed. All you have to do is send them the .vsix file you built. Be sure to build it in Release mode.

You can find the .vsix file for this extension in the StartNotepad bin directory. Assuming you’ve built the Release configuration, it will be in \D:\Code\StartNotepad\StartNotepad\bin\Release\StartNotepad.vsix.

To install the extension, the user needs to close all open instances of Visual Studio, then double-click the .vsix file to bring up the VSIX Installer. The files are copied to the %LocalAppData%\­Microsoft\VisualStudio\12.0\Extensions directory.

When the user brings up Visual Studio again, he’ll find the StartNotepad extension in Tools | Extensions and Updates. He can go to Extensions and Updates to uninstall or disable the extension.

Learning how to create extensions lets you make the Visual Studio experience truly your own. It also lets you share your best productivity enhancements and features with the community. Microsoft also loves it when you publish your extensions.

Wrapping Up

This article has described just a small part of what you can do with Visual Studio extensions. To find out more about Visual Studio extensions in general, see the “Integrate Your App or Service with Visual Studio” page at bit.ly/1zoIt59.

For a more in-depth view, see the MSDN Library articles under the “Extending Visual Studio Overview” section at bit.ly/1xWoA4k. And for some good code samples to use to build off this knowledge, check out the collection of samples on the MSDN samples gallery at bit.ly/1xWp5eD. Once you’ve built and shared a few extensions, you can truly customize your Visual Studio environment.

Susan Norwood has worked at Microsoft, mostly writing about the Visual Studio SDK. She has helped  many people get their tools integrated into Visual Studio.

Doug Erickson has worked as a developer and technical writer at Microsoft for 13 years. He looks forward to what developers will do with Visual Studio Community to develop apps and games for all platforms.

Thanks to the following Microsoft technical experts for reviewing this article: Anthony Cangialosi and Ryan Molden