A quick summary for creating a CommandBar and attaching commands to it
I’ve been spending a bunch of time trying to get everything working correctly for my Html Help Builder Add-In and I thought I’d summarize the part that’s been the most painful about this plug-in which has been getting the damn menu to behave correctly.
There are still some open issues, but at least at this point I have a consistent and stable menu structure that properly cleans up after itself.
Let’s start with some basic terminology. VS.NET 2003 uses the Office CommandBar model to create its menus and extend them. This model sucks and is documented really badly, but that’s what we’re stuck with for now. Every menu item is based on an underlying Command object that contains the base functionality that is then used to create a CommandBarControl of some sort that actually binds this Command to a real control that has display attributes. So a Command is sort of the Interface for the control, and the control is the actual instance.
There are a bunch of different controls available. The most common are CommandBarButtons which are most of the menu items you see on menus. Buttons that point at submenus are CommandBarPopups, which correspond to a CommandBar object actually. In this entry I’ll talk about these two types of objects which are used to create a new CommandBar, attach it to one of the top level menu pads (Help in this case).
To get a reference to a CommandBar you use the following Syntax:
CommandBar commandBarHelp = (CommandBar) commandBars["Help"];
Where Help is the name of the CommandBar object – in this case the Help menu popup. The CommandBar object contains a Controls collection that holds the actual items for that popup. Most of these controls will be CommandBarButtons. Now I want to add a menu to this popup.
Let’s start with the two ways to deal with commandBar object creation. One way is to create a permanent CommandBar which is added as a ‘named’ bar that Visual Studio remembers:
CommandBar commandBar = applicationObject.Commands.AddCommandBar(
HelpBuilderVsAddin.WWHELP_APPNAME,
vsCommandBarType.vsCommandBarTypeMenu,commandBarHelp,InsertionIndex)
as CommandBar;
Permanent menu pads are remembered by Visual Studio so you don’t have to reload them each time VS starts – it will load up the pads automatically and route clicks to your handlers.
If you create your add-in to hook up with:
if(connectMode == Extensibility.ext_ConnectMode.ext_cm_UISetup)
then the above should work well. The thing to remember with named Commands is that you can’t add them more than once – if they already exist VS will give an error, so you have to be very careful to clean up after yourself when the Add-In uninstalls.
However, I decided that in my application the add-in could install uninstall completely through external means and I ended up seeing inconsistencies with ext_cm_UISetup modes where the menus would not always show up properly. Instead I decided to hook menus every time VS starts which has been more reliable. So I hook my addin setup with:
if(connectMode == Extensibility.ext_ConnectMode.ext_cm_Startup )
and add my menus when VS starts every time. To do this create a menu bar that is not a named menu bar with the following code:
CommandBarControl commandBarPopup =
commandBarHelp.Controls.Add(MsoControlType.msoControlPopup, 1,"",
InsertionIndex,true);
commandBarPopup.Caption = HelpBuilderVsAddin.WWHELP_APPNAME;
commandBarPopup.BeginGroup = true;
commandBarPopup.Visible = true;
// *** Must retrieve the Command Bar Object directly via COM - not provided on the object itself
CommandBar commandBar =
commandBarPopup.GetType().InvokeMember("CommandBar",
BindingFlags.Instance | BindingFlags.GetProperty,
null,commandBarPopup,null) as CommandBar;
Note that I add a new control to the Help menu as a Popup (which gets the > and dropdown), which gives me a CommandBarPopup. Now my problem here had been how to retrieve the CommandBar object that underlies this popup, which for some unknown reason is NOT exposed by the Interop wrapper. The key is that the COM object exposes a CommandBar property that the VS.NET Interop assembly doesn’t expose, which means you have to use Reflection to retrieve the CommandBar.
Once you have the command bar you can now start adding commands.
object []contextGUIDS = new object[];
IconId = 0; // No Icon
Command command = this.DTE.Commands.AddNamedCommand(
this.addInInstance,
"ShowHelpBuilder","Show Html Help Builder","Show Html Help Builder",
IconId == 0 ? true : false, IconId,
ref contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported+
(int)vsCommandStatus.vsCommandStatusEnabled);
Once you have the Command you can create a control and attach it to a specific CommandBar:
InsertionIndex = 1;
CommandBarControl cb = command.AddControl(commandBar,InsertionIndex);
The insertion index is index where the control is to be inserted. The control is inserted before the control that matches the index. If the index is too large the control goes at the end. If you want to find a specific control to insert after you can use commandBar.Controls to either find the count or look for a specific control via its caption or control Id. Most of the VS.NET built-in controls have a fixed ID that you can reference for example. For example I search for the insertion index of the Help Menu’s Technical Support link like this:
// *** Add the menu items on the bottom of the Help menu after the Tech Support link
int InsertionIndex = 14; // default location
// *** Try to find the TechSupport ID (815)
for( x =1 ; x < commandBarHelp.Controls.Count; x++)
{
if (commandBarHelp.Controls[x].Id == HelpBuilderVsAddin.WWHELP_HELPMENU_PREVITEM_ID ) // Technical Support Item // Caption.StartsWith("&Technical Support");
{
x++;
InsertionIndex = x;
break;
}
}
Originally I searched by caption but this can cause problems if you’re running an international version of VS, so that’s not safe. Using an ID is a better choice. Be aware though that not all IDs are unique as some buttons map to a single command Id.
Since my Add-In is adding a bunch of new Commands I created a more generic routine that creates a new Command and Control in one pass with options for the most common things to set like Hotkeys and group separators. The routine takes a bunch of input parameters, but reduces the overall process to a single line of code with easy to understand parameters. It also deals with the situation where a command already exists and handles that case which would otherwise cause VS to throw an exception.
/// <summary>
/// Adds a new Command and creates a new CommandBar control both of which
/// can be returned via the AddCommandReturn object that holds refs to both.
/// </summary>
/// <param name="Name">The name of the Command. Must be handled in the Addin</param>
/// <param name="Caption">The Caption</param>
/// <param name="Description">Tooltip Text</param>
/// <param name="IconId">Icon Id if you use a custom icon. Otherwise use 0</param>
/// <param name="commandBar">The Command bar that this command will attach to</param>
/// <param name="InsertionIndex">The InsertionIndex for this CommandBar</param>
/// <param name="BeginGroup">Are we starting a new group on the toolbar (above)</param>
/// <param name="HotKey">Optional hotkey. Format: "Global::alt+f1"</param>
/// <returns>AddCommandReturn object that contains a Command and CommandBarControl object that were created</returns>
public AddCommandReturn AddCommand(string Name,string Caption, string Description,
int IconId,CommandBar commandBar, int InsertionIndex,
bool BeginGroup, string HotKey)
{
object []contextGUIDS = new object[] { };
// *** Check to see if the Command exists already to be safe
string CommandName = this.addInInstance.ProgID + "." + Name;
Command command = null;
try
{
command = this.DTE.Commands.Item(CommandName,-1);
}
catch {;}
// *** If not create it!
if (command == null)
{
command = this.DTE.Commands.AddNamedCommand(
this.addInInstance,
Name,Caption,Description,
IconId == 0 ? true : false, IconId,
ref contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported+
(int)vsCommandStatus.vsCommandStatusEnabled);
// *** If a hotkey was provided try to set it
if (HotKey != null && HotKey != "")
{
object [] bindings = (object [])command.Bindings ;
if (bindings != null)
{
bindings = new object[1];
//bindings[0] = (object)"Windows Forms Designer::alt+f1";
bindings[0] = (object) HotKey;
try
{
command.Bindings = (object) bindings;
}
catch(Exception ex) { string t = ex.Message; }
}
}
}
CommandBarControl cb = command.AddControl(commandBar,InsertionIndex);
cb.BeginGroup = BeginGroup;
return new AddCommandReturn(command,cb);
}
I use a class to return the Command and Control objects so that they can be further used or modified. Commands are mostly read only once created but you might want to use a single command for multiple controls. The class looks like this:
/// <summary>
/// Class used to return multiple return values for the AddCommand method.
/// Returns both the Command and CommandBarControl objects so the client
/// code can further customize those objects.
/// </summary>
public class AddCommandReturn
{
public AddCommandReturn(Command cmd, CommandBarControl ctrl)
{
this.Command = cmd;
this.Control = ctrl;
}
public Command Command = null;
public CommandBarControl Control = null;
}
For example in Html Help Builder various command end up on several of the Context menus .
// *** The various Context menus we will also attach to
// *** Selection: WinForm selection menu, Container Menu for Forms, Code Window for source
CommandBar ContextBar = (CommandBar) commandBars["Selection"];
CommandBar ContainerBar = (CommandBar) commandBars["Container"];
CommandBar CodeWindowBar = (CommandBar) commandBars["Code Window"];
// *** Context Menu Update Option
AddCommandReturn Return = this.AddInHelper.AddCommand("UpdateFromHelpBuilderContext",
"Update from Html Help Builder",
"Updates the Control's HelpString from Html Help Builder's current topic",
101,ContextBar,ContextInsertionIndex,false"Global::ctrl+f1");
command = Return.Command;
command.AddControl(ContainerBar,ContainerInsertionIndex);
command.AddControl(CodeWindowBar,CodeWindowInsertionIndex);
This wrapper reduces the code considerably and makes it much easier to read the OnConnection() routine (or whatever method you route to create your menu) in the startup of the Add-In.
BTW, notice the code for the Hotkey. It uses Command Bindings to attach a hotkey. This will work only if you are not using the default keyboard scheme in VS.NET. The default scheme is Read Only and doesn’t allow attachment of new keys so a copy is required first. You can do this by going into Settings, Tools | Options | Keyboard and using the Save As… option to copy the default scheme. I’m not aware of a way to do this programmatically, but it’s probably doable that way too, although this is probably not a great choice without asking the user first.
The above approach manages Commands smartly so this will work regardless of whether you install Commands once during configuration, or whether you re-create the Commands each time VS.NET starts. I prefer to do this actually because it gives more flexibility and doesn’t appear to cause anything in the way of overhead. If you do want to rebuild the Commands on each start make sure that you remove them in the OnDisconnection:
public void OnDisconnection(Extensibility.ext_DisconnectMode disconnectMode,
ref System.Array custom)
{
if(disconnectMode == Extensibility.ext_DisconnectMode.ext_dm_HostShutdown |
disconnectMode == Extensibility.ext_DisconnectMode.ext_dm_UserClosed)
{
// *** Make sure we always remove all commands
this.AddInHelper.RemoveCommand("HelpBuilder.vsAddin.ShowHelpBuilder");
this.AddInHelper.RemoveCommand("HelpBuilder.vsAddin.UpdateFromHelpBuilder");
this.AddInHelper.RemoveCommand("HelpBuilder.vsAddin.ShowHelpBuilderContext");
…
}
}
where the RemoveCommand method looks like this:
public void RemoveCommand(string Command)
{
Command cmd = null;
try
{
cmd = this.DTE.Commands.Item(Command,-1);
}
catch{;}
if (cmd != null)
cmd.Delete();
}
The wrapper here saves you from having to handle the Exception handling in your client code. Again, this applies only if you add commands on every startup in your OnConnection code. Otherwise simply leave the command in place and let AddCommand() reuse it next time around.
Command Browsing Tip
When working with Commands and Controls and trying to figure out what the names of menus and child menus are it’s often useful to see a list of the available Commands and Controls. You can use a variation of the following method to scan through all Commands and CommandBars/Controls. The code below will copy the entire command list and all CommandBars and their control captions and Ids to the clipboard. If you pass in a Caption name it will also try to locate the parent CommandBar, so you if you see a ShortCut menu you like to use but don’t know the name to just pass in the name of the caption exactly as it appears on the shortcut menu – don’t forget about the & for shortcut hotkeys.
Simply call this method from your startup code or from an debug menu option:
/// <summary>
/// Helper function that can be used to find which command bar a command lives on
/// Pass in the caption and it will tell all commandbar names it lives on.
/// </summary>
/// <param name="SearchCaption"></param>
private void ShowCommandBarByName(string SearchCaption)
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
foreach(Command oCommand in applicationObject.Commands)
{
if (oCommand.Name != null && oCommand.Name.Trim() != "")
sb.Append(oCommand.Name + "\r\n");
}
sb.Append("\r\n---------------------------------------------------------\r\n");
foreach(CommandBar oCommandBar in this.applicationObject.CommandBars)
{
sb.Append( oCommandBar.Name + "\r\n" );
foreach(CommandBarControl oControl in oCommandBar.Controls)
{
if (oControl == null || oControl.Caption.Trim() == "")
continue;
sb.Append( "\t" + oControl.Caption + " - " +
oControl.Id.ToString() + "\r\n" );
if(SearchCaption == oControl.Caption)
{
MessageBox.Show("ComamndBar: " + oCommandBar.Name +
"\r\nID: " + oControl.Id.ToString()); }
}
}
Clipboard.SetDataObject( sb.ToString() );
}
This dumps the names of all commands and all controls and their IDs to the clipboard. You can paste this into a text editor and easily see what menu options are available. The VS.NET Samples include a Command Browser that will let you do the same sort of thing interactively, but this feature is still nice because you can hide this somewhere in your app and trigger it optionally on client machines for debugging if your menus aren’t working…
None of this is big news I suppose, but I needed to write this up so I can remember it all next time I need to write an add-in. <g> This stuff is so woefully underdocumented because technically the menu/toolbar system belongs to Office and the Dev Tools, yet neither is documenting it as part of the immediate product SDKs which is really a pain. What little documentation there is incomplete and scattered about all over the place.
And finally I also want to thank Carlos Quintero, who helped me tremendously and whose VS.NET add-in MSDN KB entries were of most help in solving my problems however simple they might have been.