Tuesday, March 27, 2007

How to add a command to a custom VS project type

In this post I'm going to show how to add your own commands to a custom project type's context menu in Visual Studio.

I've been having a lot of fun recently trying to create my own custom project type for VS. In my last post on this subject I described how you can create the most basic custom project type with no real functionality. When you create one of these with the VS SDK's MPF (Managed Package Framework), your project inherits from ProjectNode which seems to be designed to support a buildable project. This makes sense since VS is fundamentally a development tool and the vast majority of project types are going to be things that need to be built. This means that when you right click on your new project, it shows a context menu with commands like 'Set As Startup Project' and 'Debug' and there are hooks in the ProjectNode class to implement this behaviour. However the kind of project I want to create is more like a database project where there's no requirement to actually build anything and the project can't be run or debugged, unfortunately the only way to remove all the build and debug related functionality seems to be to create your own version of ProjectNode (and ProjectFactory too). I haven't explored this yet, so if you walk through this post to create your own context menu items as I describe you'll see those commands still displayed. However, they don't do anything if you select them. OK, so to start off you need to create your new project type following the steps in the last post. But we have to make one change. When the New Package Wizard runs, instead of leaving all the checkboxes blank on the 'Select VSPackage Options' page, you should check the 'Menu Command' box. When the wizard runs, in addtion to all the other project items, it creates a folder called 'CtcComponents' that includes the following files: CommandIds.h Guids.h Resource.h TestMenuPackage.ctc (or whatever you_called_your_project.ctc) If you right click on the ctc file and select properties you'll see that its build action is 'CtcFile'. This file has its own special compiler that builds a binary command table file that VS uses to build all the menus and toolbars you see in the IDE. The CtcFile compiler uses the C++ preprocessor, thus the C style header files, #defines and #includes. When the wizard builds the package project it creates a command for you that appears in the Tools menu. You can see how it's defined by looking in the ctc files. Basically, in the ctc file you define a command set with a unique guid and then a command group that belongs to that command set and a particular VS menu, then define some commands that belong to the command group. To have a command group that appears under your custom project type, you first need to define the command set guid in Guids.h (I just used the one that the wizard created)

// Command set guid for our commands (used with IOleCommandTarget)

// { bdc32849-d202-496c-96b4-837c0254e0e2 }

#define guidTestMenuPackageCmdSet { 0xBDC32849, 0xD202, 0x496C, { 0x96, 0xB4, 0x83, 0x7C, 0x2, 0x54, 0xE0, 0xE2 } }

#ifdef DEFINE_GUID

DEFINE_GUID(CLSID_TestMenuPackageCmdSet,

0xBDC32849, 0xD202, 0x496C, 0x96, 0xB4, 0x83, 0x7C, 0x2, 0x54, 0xE0, 0xE2 );

#endif

The C style syntax is a bit gnarly I agree, but you can get the Toos->Create Guid tool to generate it for you which is nice. Next you define the command group id and command id in the CommandIds.h file:

///////////////////////////////////////////////////////////////////////////////

// Menu Group IDs



#define MyProjectGroup    0x10A0



///////////////////////////////////////////////////////////////////////////////

// Command IDs



#define cmdidDoSomething   0x001

Next you link the command group to the command set and a VS menu in the ctc file under the NEWGROUPS_BEGIN section:

// my project group

guidTestMenuPackageCmdSet:MyProjectGroup, // group name

guidSHLMainMenu:IDM_VS_CTXT_PROJNODE,  // parent group

0x0001;          // priority

The parent group 'guidSHLMainMenu:IDM_VS_CTXT_PROJNODE' tells VS that this command group should appear under project nodes in the solution explorer. OK, so how do find out the name of that constant? It's a case of digging in a file called 'SharedCmdPlace.ctc' that's located at C:\Program Files\Visual Studio 2005 SDK\2007.02\VisualStudioIntegration\Common\Inc on my computer and having a best guess about which one seems to fit the bill. The next step is to link define the command in the ctc file under the BUTTONS_BEGIN section (all menu items are BUTTONs):

// Do Something Fun command

guidTestMenuPackageCmdSet:cmdidDoSomething,  // command

guidTestMenuPackageCmdSet:MyProjectGroup,  // parent group

0x0100,           // priority

guidTestMenuPackageCmdSet:bmpPic1,    // image

BUTTON,           // type

DEFAULTINVISIBLE  DYNAMICVISIBILITY,   // visibility

"Do Something Fun";        // caption

You can see we link the command id to our command group, give it a display priority (which controls where it appears in the command group), an icon (I've just used the one created by the wizard), define the visibility and the caption. The visibility is important. When we defined our command group we told VS that we wanted it to appear under a project node. This means that it will appear under every project node, not only ours. We need to have our command default to being invisible and then enable it only from our project node, that's why the visibility is set to 'DEFAULTINVISIBLE DYNAMICVISIBILITY'. OK, that's the ctc file done, now we can get our project node to enable the command and respond to it when it's clicked. Our project class (referring back to the previous post) inherits from ProjectNode which in turn inherits from HierarchyNode which implements IOleCommandTarget. If you read the documentation on VSPackages you'll see that IOleCommandTarget is the interface for any class that needs to intercept command invocations. IOleCommandTarget defines two methods, QueryStatus and Exec. These are handled by HierarchyNode and you can customise their behaviour by overriding QueryStatusOnNode and ExecCommandOnNode. QueryStatus gives the implementer of IOleCommandTarget a chance to modify the display of the menu item and is invoked whenever the project node becomes active. Because the default visibility of our command is 'DEFAULTINVISIBLE DYNAMICVISIBILITY', we need to enable it when our project is active by overriding QueryStatusOnNode in our project class:

protected override int QueryStatusOnNode(

    Guid guidCmdGroup, uint cmd, IntPtr pCmdText, ref QueryStatusResult result)

{

    if (guidCmdGroup == GuidList.guidTestMenuPackageCmdSet)

    {

        result = QueryStatusResult.SUPPORTED  QueryStatusResult.ENABLED;

        return VSConstants.S_OK;

    }

    return base.QueryStatusOnNode(guidCmdGroup, cmd, pCmdText, ref result);

}

We can find out if the command is one of ours by checking the guidCmdGroup argument against the guid for our command set (I've used the GuidList static class that's generated by the wizard for this). Any other commands we just delegate to the base class. Now if we run our project by hitting F5 and create a new instance of our custom project type we'll see our new command in the context menu of the project node. To respond when a user clicks on our new command we simply override ExecCommandOnNode:

protected override int ExecCommandOnNode(

    Guid guidCmdGroup, uint cmd, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)

{

    if (guidCmdGroup == GuidList.guidTestMenuPackageCmdSet)

    {

        if (cmd == 13)

        {

            return ShowMessage();

        }

    }

    return base.ExecCommandOnNode(guidCmdGroup, cmd, nCmdexecopt, pvaIn, pvaOut);

}

Once again we look for any of our own commands by checking the guidCmdGroup argument and pick individual commands by checking the 'cmd' argument which holds the command id. This is kinda redundant here because we've only got the one command. ShowMessage is just a private function that pops up a message box in this case, but it's where you'd implement whatever functionality you wanted your command to execute. Again we delegate any other commands to the base class. And that's all there is to it. Now I know I'm going to start sounding like a broken record, but this VSPackage stuff is made far too hard by poor documentation. Needless to say, I spent ages wondering why I couldn't see my command as I debugged through QueryStatusOnNode, and that was after I spent just as long working out that I should be overriding QueryStatusOnNode. As for finding the IDM_VS_CTXT_PROJNODE constant, that was just luck. All the project base stuff really does need some kind of high level overview. Even the best samples are very hard to grep without it.

No comments: