Dynamic Menu Commands in Visual Studio Packages – Part 3

This is the final post in a series detailing how to build dynamic menu commands in Visual Studio packages. The previous posts are located here:

  • Part 1 – Discusses UI Contexts and how to utilize the built-in ones for dynamic menu commands.
  • Part 2 – Discusses the use of the BeforyQueryStatus event to provide more flexibility than built-in UI contexts.

What we’ve explored to date is a way to provide dynamic menu commands (e.g. dynamic visibility, enabled state) in our own package, and the techniques that I’ve shown thus far have worked well for this scenario. However, if you want to develop multiple menu commands or even multiple packages that rely on a custom condition (as shown in Part 2), then you’re stuck implementing the same logic in an event handler for the BeforeQueryStatus event for each OleMenuCommand.

Wouldn’t it be great if we could create our own UI Context, similar to the built-in ones like UIContext_NoSolution or UIContext_FullScreenMode? That way we can create multiple menu commands that rely on that context or even multiple packages which rely on it.

And that’s exactly what this post will cover. We’ll use the solution starting from where we left off at the end of Part 2.

Step 1: Creating New Commands

Because Part 2 covered how to create a new menu command from scratch using the VSCT file and the appropriate procedural code, I’m not going to cover the process in depth here. I’ll be creating two new menu commands, which will be part of the command set that we created in Part 2 (guidDynamicMenuDevelopmentCmdSetPart2 in the GuidList class). In the snippets that follow, the bold sections indicate what I added to achieve this:

PkgCmdIDList Class (in PkgCmdID.cs)

static class PkgCmdIDList

{

public const uint cmdidBuiltInUIContext =        0×100;

public const uint cmdidQueryStatus      =        0×101;

public const uint cmdidCustomUIContext  =        0×102;

public const uint cmdidCustomUIContext2 =        0×103;

};

DynamicMenuDevelopment.vsct File – Symbols Element

<GuidSymbol name=guidDynamicMenuDevelopmentCmdSetPart2 value={9d9046da-94f8-4fd0-8a00-92bf4f6defa8}>

<IDSymbol name=menuidQueryStatusGroup value=0×1020/>

<IDSymbol name=cmdidQueryStatus value=0×0101 />

<IDSymbol name=cmdidCustomUIContext value=0×0102 />

<IDSymbol name=cmdidCustomUIContext2 value=0×0103 />

</GuidSymbol>

DynamicMenuDevelopment.vsct File – Buttons Element

<Button guid=guidDynamicMenuDevelopmentCmdSetPart2 id=cmdidCustomUIContext priority=0×1 type=Button>

<Parent guid=guidDynamicMenuDevelopmentCmdSetPart2 id=menuidQueryStatusGroup/>

<Icon guid=guidImages id=bmpPicSearch/>

<CommandFlag>DynamicVisibility</CommandFlag>

<Strings>

<CommandName>cmdidCustomUIContext</CommandName>


<ButtonText>Custom UI Context 1</ButtonText>

</Strings>

</Button>

<Button guid=guidDynamicMenuDevelopmentCmdSetPart2 id=cmdidCustomUIContext2 priority=0×2 type=Button>

<Parent guid=guidDynamicMenuDevelopmentCmdSetPart2 id=menuidQueryStatusGroup/>

<Icon guid=guidImages id=bmpPicX/>

<CommandFlag>DynamicVisibility</CommandFlag>

<Strings>

<CommandName>cmdidCustomUIContext2</CommandName>

<ButtonText>Custom UI Context 2</ButtonText>

</Strings>

</Button>

In summary, I added two IDs to the PkgCmdIDList class which specify the IDs of the two new menu commands I’m going to add. Furthermore, I registered these IDs in the VSCT file using IDSymbol elements in the guidDynamicMenuDevelopmentCmdSetPart2 GuidSymbol (which I mentioned earlier). Finally, I added two buttons to the Buttons element that map directly to the commands we created. At this point, we could create handlers for these commands, but it’s really not necessary for us to achieve what we want. If you press F5 and open a project that has at least one project item in it, you’ll see our two new menu commands when right clicking on a project item:

image

(These menu commands appear in the Solution Explorer because we defined their parents as being in the Solution Explorer context menu—see Post 2 for details.)

Step 2: Implementing the Custom UI Context

For our custom UI context, let’s re-implement the functionality that the second command we created (in Post 2) did—display itself when a .dbml file is selected in the Solution Explorer but hide itself when anything else is selected. To do this we’ll need a way to tell Visual Studio what the current UI Context is. Fortunately, the SVsShellMonitorSelection service, through the IVsMonitorSelection interface, exposes the functionality to get and set the current UI context (as well as means to track the current selection). This will definitely fit our needs, but how do we receive notifications when the current selection changes?

This is where the IVsSelectionEvents interface really comes in handy. It doesn’t correspond to a specific Visual Studio service; instead, it’s used in tandem with the IVsMonitorSelection interface to receive notifications when the UI context has changed, when the current selection has changed, and when an element value has changed. The last notification is outside of the scope of this article, but this blog post may help you understand its purpose.

Is what we must do becoming clearer to you? If not, maybe the following steps will help:

  1. Create a UI context GUID that we can use for DBML file selection.
  2. Implement the IVsSelectionEvents interface.
  3. Use that implementation with the SVsShellMonitorSelection service to listen for selection events.
  4. When the selection changes to a DBML file, set the UI context that we created in Step 1 as the active UI context.
  5. Of course, the final step is to hook up our
    menu commands to this UI context to test our work.

Creating the UI Context GUID

This is probably the easiest step out of the five, as all it does is involve our creating a new GUID to put in the GuidList class that we can reference later. I’ll do this using guidgen.exe, accessible through the Tools > Create GUID option in Visual Studio. The result looks like this:

static class GuidList

{

public const string guidDynamicMenuDevelopmentPkgString = “d626ae3e-6eaa-414f-9a74-4f41fb902a23″;

public const string guidDynamicMenuDevelopmentCmdSetString = “a9d25ef1-3235-4a08-8c93-f26619635e91″;

public const string guidDynamicMenuDevelopmentCmdSetPart2String = “9d9046da-94f8-4fd0-8a00-92bf4f6defa8″;

public const string UICONTEXT_DbmlFileSelectedString = “203116D4-FC70-48d8-A4E8-2467F58B1F65″;

public static readonly Guid guidDynamicMenuDevelopmentCmdSet = new Guid(guidDynamicMenuDevelopmentCmdSetString);

public static readonly Guid guidDynamicMenuDevelopmentCmdSetPart2 = new Guid(guidDynamicMenuDevelopmentCmdSetPart2String);

public static readonly Guid UICONTEXT_DbmlFileSelected = new Guid(UICONTEXT_DbmlFileSelectedString);

};

Implementing the IVsSelectionEvents Interface

This interface is fairly straightforward to implement. Before getting into the code, remember that we are checking what the new selection is when it changes. If it’s a DBML file in the Solution Explorer, then we activate our UI context. Here’s what my implementation looks like:

/// <summary>

/// Our implementation of the <see cref=”IVsSelectionEvents”/> interface,

/// used to set a custom UI context GUID.

/// </summary>

private class SelectionEvents : IVsSelectionEvents

{

private static IVsMonitorSelection SelectionService;

private static uint ContextCookie = RegisterContext();

private static uint RegisterContext()

{

// Initialize the selection service

SelectionService = (IVsMonitorSelection)Package.GetGlobalService(typeof(SVsShellMonitorSelection));

// Get a cookie for our UI Context. This “registers” our

// UI context with the selection service so we can set it again later.

uint retVal;

Guid uiContext = GuidList.UICONTEXT_DbmlFileSelected;

SelectionService.GetCmdUIContextCookie(ref uiContext, out retVal);

return retVal;

}

// We don’t care about either of these methods, but it’s useful to know what they do.

int IVsSelectionEvents.OnCmdUIContextChanged(uint dwCmdUICookie, int fActive)

{

return VSConstants.S_OK;

}

int IVsSelectionEvents.OnElementValueChanged(uint elementid, object varValueOld, object varValueNew)

{

return VSConstants.S_OK;

}

int IVsSelectionEvents.OnSelectionChanged(IVsHierarchy pHierOld, uint itemidOld,

IVsMultiItemSelect pMISOld, ISelectionContainer pSCOld,

IVsHierarchy pHierNew, uint itemidNew,

IVsMultiItemSelect pMISNew, ISelectionContainer pSCNew)

{

if (pHierNew != null)

{

object fileName;

pHierNew.GetProperty(itemidNew, (int)__VSHPROPID.VSHPROPID_Name, out fileName);

if (fileName != null && fileName.ToString().EndsWith(“.dbml”))

{

// If we meet the conditions, set the UI context to be active.

SelectionService.SetCmdUIContext(ContextCookie, 1);

return VSConstants.S_OK;

}

}

// Otherwise, deactivate it.

SelectionService.SetCmdUIContext(ContextCookie, 0);

return VSConstants.S_OK;

}

}

The implementation should make sense based off my previous article. When the selection changes, we inspect the new IVsHierarchy to find its selection. If the name of the selection ends with “.dbml”, then we are dealing with a DBML file. We activate the UI context in this case, and deactivate it in all other cases. Before any of that happens, though, we have to register the UI context GUID with the SVsShellMonitorSelection service, which is what the RegisterContext() method does. The cookie that we receive from the call to IVsMonitorSelection.GetCmdUIContextCookie() is then used to set the UI context later on. There’s a bit of a trickery involved here to understand what’s going on, but most of it should be self-evident.

Registering the Selection Event Listener

Now that we have a listener for the selection events, we need to register it with the selection service. To do this, add this to the Initialize() method in our package:

mySelectionEvents = new SelectionEvents();

IVsMonitorSelection selectionService = (IVsMonitorSelection)this.GetService(typeof(SVsShellMonitorSelection));

selectionService.AdviseSelectionEvents(mySelectionEvents, out mySelectionEventsCookie);

Here, mySelectionEvents and mySelectionEventsCookie are private fields in the package class. The only unfamiliar territory is the IVsMonitorSelection.AdviseSelectionEvents method. Now, I am by no means a COM expert, but this pattern of advising for events and then unadvising for them later on seems to be a common task for applications that interact with COM. The fact that we can unadvise for the events implies that there is a unique identifier for an “advise.” This unique identifier is the cookie that is passed by reference into the call for advising for selection events. We store this value in a private field so we can unadvise at an appropriate time i.e. in the package’s Dispose method:

protected override void Dispose(bool disposing)

{

IVsMonitorSelection selectionService = (IVsMonitorSelection)this.Ge
tService(typeof(SVsShellMonitorSelection));

if (selectionService != null)

{

selectionService.UnadviseSelectionEvents(mySelectionEventsCookie);

}

base.Dispose(disposing);

}

Hooking Up the Menu Commands

It’s been a long toll, but we’re almost to the end! The last step is to hook up our two menu commands to the UI context we created. Part 1 shows how to do this with built-in UI contexts, and doing it with a custom one is no different. The first step is to add the GUID for our custom UI context to the Symbols element. Then, we add VisibilityItem elements to the VisibilityConstraints section which hook up the menu commands to that context.

The GUID Symbol for the Custom UI Context

<GuidSymbol name=UICONTEXT_DbmlFileSelected value={203116D4-FC70-48d8-A4E8-2467F58B1F65} />

The Visibility Constraints

<VisibilityConstraints>

<VisibilityItem guid=guidDynamicMenuDevelopmentCmdSet id=cmdidBuiltInUIContext context=UICONTEXT_NoSolution />

<VisibilityItem guid=guidDynamicMenuDevelopmentCmdSetPart2 id=cmdidCustomUIContext context=UICONTEXT_DbmlFileSelected />

<VisibilityItem guid=guidDynamicMenuDevelopmentCmdSetPart2 id=cmdidCustomUIContext2 context=UICONTEXT_DbmlFileSelected />

</VisibilityConstraints>

Step 3: The Results!

Now that we’ve gone through that rigmarole, press F5 and load a project with a DBML file in it (or create a DBML file in some file). You’ll see that our two new menu commands (as well the menu command we implemented with the BeforeQueryStatus event handler in Part 2) show up only when right clicking on a DBML file.

image

This concludes both this article on building custom UI contexts and this series on dynamic menus in Visual Studio. I hope this and the past articles help in you Visual Studio Package development!

Associated Solution

Other Articles in the Series

12 thoughts on “Dynamic Menu Commands in Visual Studio Packages – Part 3

  1. Hi there,
    It is really a great assets for the VS programmers. It helped me a lot. But i have a small question. How can I handle the event generated for click event in the menu created in part 2 as well as part 3. I am using this method to add a new item to the existing context menu.
    Thanks, Manish

  2. Hi Manish, thanks for the comments. The menu items shown in all of the articles are handled in the same way through the IMenuCommandService (see Part 2). If you add a MenuCommand to the IMenuCommandService, then any menu item that matches the ID of that MenuCommand will cause that MenuCommand to be invoked. The MenuCommand constructor takes an EventHandler, which you can use to handle the event when the command is invoked.

    In this article I didn’t show how to handle the command, but you can do so in the same way as in part 2.

    I hope that helps.
    David

  3. Hi David,

    In addition to your response to Manish’s question. When I add the command to the IMenuCommandService (to handle the commands ‘click’ event’) the dynamic showing and hiding of the command ceases to function (it’s always displayed regardless). Do you experience this as well?

    Cheers!
    Kilpatrick

  4. Kilpatrick,

    Yes I do. Perhaps this info from the docs can help:

    The VisibilityItem element determines the static visibility of commands and toolbars. Each entry identifies a command or menu and an associated command user interface (UI) context. The visibility is first controlled by the Visual Studio integrated development environment (IDE) without loading the VSPackage.

    After your VSPackage is loaded, the IDE relies on your commands to determine their visibility instead of the static visibility defined in VisibilityItem. In an unmanaged VSPackage, implement QueryStatus; in a managed VSPackage, handle the BeforeQueryStatus event.

    http://msdn.microsoft.com/en-us/library/bb491710.aspx

    Looks as though we’ll have to use the technique in Part 2 for true dynamic menus…

    Regards,
    David

  5. Hi David,

    Thanks for the clarification and it looks like you’re right. When I implement the technique in Part 2 the command will be dynamic and I can also hook it up to a callback event. It’s a shame that I couldn’t use the technique as defined in Part 3. This would’ve worked well when dynamically displaying commands and menus based on the same criteria. Although I’m sure that I will find a way of doing the same using the technique in Part 2.

    Thanks for the speedy reply as well!

    Cheers!
    Craig Wilson

  6. Hi David,

    I would like to ask for your help about how can I override the command when the user clicks an item/file in the solution explorer.

    For example:

    the user click the Sample.vb code file then a dialog will be opened.

    Thank you,
    Gil Ballesca.

  7. Gil, are you saying to launch a dialog when someone clicks on file to select it in the Solution Explorer? (Not when a context menu item is chosen?) To answer your question, I’m not sure how to do this; it’s unrelated to what I have discussed in this series of posts.

    Also, just my 2 cents, but I don’t think launching a dialog is a great experience for the end user in this case.

  8. David,

    First, thanks for the article – it’s been very informative! I was trying to implement a toolbar that would only enable commands when a specific window was open, and since your “BeforeQueryStatus” from Part 2 worked so well for my menu items I tried again. The problem that I discovered is that toolbar buttons don’t fire the BeforeQueryStatus event, so you can not disable them that way. I went back to looking at the VisibilityItem and custom Context approach that you outlined here and I think I discovered how to avoid the problem that Kilpatrick described above (losing control when you add it to the IMenuCommandService).

    Instead of using the IMenuCommandService and “Adding” commands to it, you can implement the IOleCommandTarget interface on your package class itself. Doing that doesn’t invalidate your contexts and the IDE will continue to enable/disable your commands when the context changes. (The toolbar buttons will not disappear, but menu commands do!)

    To implement this interface, you only need to add two methods as outlined here:
    int IOleCommandTarget.Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
    {
    //If it’s not your GUID, then say so.
    if (pguidCmdGroup != GuidList.guidDynamicMenuDevelopmentCmdSetPart2)
    {
    return (int)Constants.OLECMDERR_E_UNKNOWNGROUP;
    }
    //If it’s not one of your comamnds, then say so.
    if (nCmdID != PkgCmdIDList.cmdidCustomUIContext)
    {
    return (int)Constants.OLECMDERR_E_NOTSUPPORTED;
    }
    //This returns an error if someone requests “Help” for this command, but you have not implemented it.
    if (nCmdexecopt == (uint)OLECMDEXECOPT.OLECMDEXECOPT_SHOWHELP)
    {
    return (int)Constants.OLECMDERR_E_NOHELP;
    }
    if (nCmdID == PkgCmdIDList.cmdidCustomUIContext)
    {
    //Add your code here to handle the command
    }
    return VSConstants.S_OK;
    }

    int IOleCommandTarget.QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
    {
    //If it’s not your GUID, then say so.
    if (pguidCmdGroup != GuidList.guidDynamicMenuDevelopmentCmdSetPart2)
    {
    return (int)Constants.OLECMDERR_E_UNKNOWNGROUP;
    }
    //If it’s not one of your comamnds, then say so.
    if (prgCmds[0].cmdID != PkgCmdIDList.cmdidCustomUIContext)
    {
    return (int)Constants.OLECMDERR_E_NOTSUPPORTED;
    }
    //Additional code can be added here to more finely control the display
    //See OLECMDF enumeration for more details.
    return VSConstants.S_OK;
    }

    (NOTE: The IOleCommandTarget. prefix is added to the methods because the Package already implements IOleCommandTarget, but this forces the package to override that implementation.)

    Now you can remove all references to the IMenuCommandService and the code associated with “Adding” your commands from the package initialization.

    Thanks again for the great article, and I hope this helps!

    • Hi Tom, thanks for the info! I haven’t done VS package development in a while but I’ll definitely use this code if I run into the same situation. Thanks again!

  9. Hi Tom,

    this is a great tutorial. But i have a doubt. I have changed the the location of the commands to the menu tables of Server Explorer. And now the problem, how can i get the selected node in MenuItemCallback?

    thank you very much

  10. Pingback: JSLint in Visual Studio Part 2 | Scott Logic

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>