Who said building Visual Studio Extensions was hard?

comments

In years past building Visual Studio Extensions have often been considered the realm of the big boys. Staff working at Jetbrains or the Microsoft employees of the world. Last year I saw a talk given by Mads Kristensen aimed at taking away some of this stigma and showing how easy the guys at Microsoft have tried to make it for developers like you and me to just up and write extensions. I’ve been wanting to build one ever since, but haven’t had a good enough excuse to jump right in – until now. Here follows the creation of “OnCheckin Web.config Transformer”.

imageMy little project’s requirements

Last year I launched my own SAAS startup OnCheckin to bring the time and money saving gift of deployment automation to the masses.

A recent release has added support for multiple environments for each deployment project. With this comes the addition of environmental based config transforms on top of the already supported “web.oncheckin.config” transform applied to all build and deploys done through OnCheckin.

The way this works is a tiered transformation of your web.config.

If you have an environment in your deployment workflow called Production and you want to store database connection strings etc. that are environment specific then you’ll need to add a config transform named “web.production.config” to your project.

Web.config transforms are then applied in the following order.

  1. web.release.config
  2. web.oncheckin.config
  3. web.production.config

imageThis is great, but unless you have a publishing profile in your website called “production” creating the above transform is actually a little more difficult, and involves a bit of fiddling with the actual XML in your web application’s project file.

Like a lot of learning project’s, when you have your own itch to scratch it’s often the best way to start.

What you’ll need to get started

Firs you’ll need the following

Once you’ve got these installed you can jump right in.

Create a new project under Visual C# > Extensibility > Visual Studio Package.

image

Click through the opening wizard.

image

Select a language for your extension and either provide or select to enter a signing key.

image

Enter some basic information about your plugin and provide an icon.

image

Then select “Menu Command” from the next window – this will create the boiler plate code to get us started.

image

Then enter the text for your first command option and give it a command id (you’ll understand this later).

image

Select whether you want a Microsoft Unit test and Integration project to get you started (yes, please!).

image

Then click “Finish”.

This has actually created for you a working Menu item VSIX project.

If you “Run” the project a new instance of a sandboxed “Visual Studio Experimental Instance” will start with your menu plugin installed. Open a project and then select the “Tools” menu drop down to see your plugin.

image

If I click this I get the default method created by the template firing.

image

You can find this code inside the class “OnCheckinTransforms.VisualStudioPackage.cs” automatically created by the project setup.

private void MenuItemCallback(object sender, EventArgs e)
{
    // Show a Message Box to prove we were here
    IVsUIShell uiShell = (IVsUIShell)GetService(typeof(SVsUIShell));
    Guid clsid = Guid.Empty;
    int result;
    Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(uiShell.ShowMessageBox(
                0,
                ref clsid,
                "OnCheckin Transforms",
                string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()", this.ToString()),
                string.Empty,
                0,
                OLEMSGBUTTON.OLEMSGBUTTON_OK,
                OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST,
                OLEMSGICON.OLEMSGICON_INFO,
                0,        // false
                out result));
}

Moving along - what we want to do it change this from a menu item to a context menu item for files in your solution, so when you right click a file (our final goal is just a web.config) you see our menu. Let’s change this.

Open “OnCheckinTransforms.VisualStudio.vsct” and modify the menu group created for your action to make it an “Item node menu” command instead of a “Visual Studio Menu” command.

<Group guid="guidOnCheckinTransforms_VisualStudioCmdSet" id="MyMenuGroup" priority="0x0600">
  <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_ITEMNODE"/>
  <!--<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>-->
</Group>

Then immediately upon clicking “Start” on my project another instance of Visual Studio will launch with your plugin installed.

You’ll notice now that if I right click on a project item (any item), I’ll see my command option.

image

We’re moving along pretty quickly, but what we really want now is:

  • Only show our menu if a project item is selected (not a folder, or project etc).
  • Disable our menu if you have selected a file that isn’t a web.config, or is a child of a web.config.

To do the above we can hook up an event that fires before the context menu shows on the screen. This means we can hide or disable our menu through code based on the file selected.

The first thing we’ll need to do is turn on the features to disable and hide our menu item by default. To do this open up the “OnCheckinTransforms.VisualStudio.vsct” file again and add a few lines to our menu button.

<Button guid="guidOnCheckinTransforms_VisualStudioCmdSet" id="oncheckinEnvTransform" priority="0x0100" type="Button">
  <Parent guid="guidOnCheckinTransforms_VisualStudioCmdSet" id="MyMenuGroup" />
  <Icon guid="guidImages" id="bmpPic1" />
  <!-- the 2 lines below set the default visibility-->
  <CommandFlag>DefaultInvisible</CommandFlag>
  <CommandFlag>DynamicVisibility</CommandFlag>
    
  <Strings>
    <ButtonText>Add EnvironmentTransforms</ButtonText>
  </Strings>
</Button>

Then we open our ‘OnCheckinTransforms.VisualStudioPackage.cs’ file again and replace a few lines in our Initialize method. We change our menu command’s type, and then hook into a BeforeQueryStatus event handler.

OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
if ( null != mcs )
{
    // Create the command for the menu item.
    CommandID menuCommandID = new CommandID(GuidList.guidOnCheckinTransforms_VisualStudioCmdSet, (int)PkgCmdIDList.oncheckinEnvTransform);
                
    // WE COMMENT OUT THE LINE BELOW
    // MenuCommand menuItem = new MenuCommand(MenuItemCallback, menuCommandID );
                
    // AND REPLACE IT WITH A DIFFERENT TYPE
    var menuItem = new OleMenuCommand(MenuItemCallback, menuCommandID);
    menuItem.BeforeQueryStatus += menuCommand_BeforeQueryStatus;

    mcs.AddCommand( menuItem );
}

Then we add a new method to handle changing the status of our menu item and check if the filename is ‘web.config’ before showing.

void menuCommand_BeforeQueryStatus(object sender, EventArgs e)
{
    // get the menu that fired the event
    var menuCommand = sender as OleMenuCommand;
    if (menuCommand != null)
    {
        // start by assuming that the menu will not be shown
        menuCommand.Visible = false;
        menuCommand.Enabled = false;

        IVsHierarchy hierarchy = null;
        uint itemid = VSConstants.VSITEMID_NIL;

        if (!IsSingleProjectItemSelection(out hierarchy, out itemid)) return;
        // Get the file path
        string itemFullPath = null;
        ((IVsProject) hierarchy).GetMkDocument(itemid, out itemFullPath);
        var transformFileInfo = new FileInfo(itemFullPath);

        // then check if the file is named 'web.config'
        bool isWebConfig = string.Compare("web.config", transformFileInfo.Name, StringComparison.OrdinalIgnoreCase) == 0;

        // if not leave the menu hidden
        if (!isWebConfig) return;

        menuCommand.Visible = true;
        menuCommand.Enabled = true;
    }
}
public static bool IsSingleProjectItemSelection(out IVsHierarchy hierarchy, out uint itemid)
{
    hierarchy = null;
    itemid = VSConstants.VSITEMID_NIL;
    int hr = VSConstants.S_OK;

    var monitorSelection = Package.GetGlobalService(typeof(SVsShellMonitorSelection)) as IVsMonitorSelection;
    var solution = Package.GetGlobalService(typeof(SVsSolution)) as IVsSolution;
    if (monitorSelection == null || solution == null)
    {
        return false;
    }

    IVsMultiItemSelect multiItemSelect = null;
    IntPtr hierarchyPtr = IntPtr.Zero;
    IntPtr selectionContainerPtr = IntPtr.Zero;

    try
    {
        hr = monitorSelection.GetCurrentSelection(out hierarchyPtr, out itemid, out multiItemSelect, out selectionContainerPtr);

        if (ErrorHandler.Failed(hr) || hierarchyPtr == IntPtr.Zero || itemid == VSConstants.VSITEMID_NIL)
        {
            // there is no selection
            return false;
        }

        // multiple items are selected
        if (multiItemSelect != null) return false;

        // there is a hierarchy root node selected, thus it is not a single item inside a project

        if (itemid == VSConstants.VSITEMID_ROOT) return false;

        hierarchy = Marshal.GetObjectForIUnknown(hierarchyPtr) as IVsHierarchy;
        if (hierarchy == null) return false;

        Guid guidProjectID = Guid.Empty;

        if (ErrorHandler.Failed(solution.GetGuidOfProject(hierarchy, out guidProjectID)))
        {
            return false; // hierarchy is not a project inside the Solution if it does not have a ProjectID Guid
        }

        // if we got this far then there is a single project item selected
        return true;
    }
    finally
    {
        if (selectionContainerPtr != IntPtr.Zero)
        {
            Marshal.Release(selectionContainerPtr);
        }

        if (hierarchyPtr != IntPtr.Zero)
        {
            Marshal.Release(hierarchyPtr);
        }
    }
}
Now we have our extension only showing up we right click a web.config file, and all other files will hide/disable the extension menu option.

The rest of the code required to replace the click handler with code to add a web.config transform is included in the Github repository at the end, it gets a bit tedious to past inside a post.

To continue your journey you can take a look at the VSIX documentation over on MSDN.

Publishing your VSIX

Once you’ve got your extension to a place where you’re happy, it’s time to get it out there for other developers to use. You want to publish your extension in the Visual Studio Extension Gallery.

First, build your extension in release mode, then head on over to http://visualstudiogallery.msdn.microsoft.com/

Login with the Microsoft account you want to publish using.

Then click on the big “Upload” button on the home page.

image

On the second page, select “Tool” as the extension type you’re uploading.

image

Then surf to your ‘/bin/release’ directory  and select your VSIX for upload.

image

Then on the next page enter a description, select some categories and select “Publish” and you’re done!

image

But wait, there’s more…

Screen2

My final VSIX was a little more involved than the above show as I extend mine to actually contain a WPF window and some more logic to add a web.config transform. As I also reused some of the great codebase over on Sayed Hashimi’s project Slow Cheetah as part of my project and Sayed’s project is open sourced using the Apache 2.0 license, I’ve decided to open source my project as well.

You can all the source code for it over here on Github – also licensed as Apache 2.0 so you can reuse and learn forevermore.

My final Visual Studio plugin is also now online for you to download and use, and it can be found here.

If you’d like to give the new release of OnCheckin.com a try feel free to head on over and signup today!