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:
(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:
- Create a UI context GUID that we can use for DBML file selection.
- Implement the IVsSelectionEvents interface.
- Use that implementation with the SVsShellMonitorSelection service to listen for selection events.
- When the selection changes to a DBML file, set the UI context that we created in Step 1 as the active UI context.
- 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.
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!
Syndication
Leave a Reply