Chapter 7. Exploring Commands Programmatically

Commands are the most fundamental mechanism of communication between the user and the Microsoft® Visual Studio® integrated development environment (IDE). In this chapter, we'll explore how you can use existing commands as well as create your own commands by using add-ins and macros.

What Is a Command?

If you've written user interface software for the Microsoft Windows® operating system, you're probably familiar with the event-driven programming model. When the user clicks a button on a form, chooses a menu item, or presses a key on the keyboard, your program receives a notification of that user action. If you're programming at the Windows SDK level, such as with the Microsoft Visual C++® programming language, when the user performs this action, your program receives a message detailing what happened. If you're using a language such as Microsoft Visual Basic® or Microsoft Visual C#®, this notification happens in the form of an event handler being called. These notifications are commands issued by the user, and the program carries out this command by performing some action for the user.

Visual Studio uses a method of notification similar to that of Win32® message-passing to inform code as the user interacts with the IDE. However, because of the complexity and number of commands available in the Visual Studio IDE, command routing, or passing a notification to the proper handler of that notification, isn't as simple as receiving a message. For instance, suppose the user chooses File | New | File. Because there are a number of different add-on programs (not to be confused with add-ins), such as Visual C++, Visual Basic, and Visual C#, Visual Studio needs to determine which of these programs handles this menu item choice. When a Win32 program handles a message, one message loop handles that message, but because there are a number of possible handlers of a command in Visual Studio, commands need to be routed to the correct code. Each of these add-on products reserves a globally unique identifier (GUID) to uniquely identify itself, and each command that is available associates itself with the GUID of a particular add-on. When a user executes a command, the GUID for that command is retrieved, the add-on program that handles that GUID is found, and the command is sent to that add-on.

A command also needs another part to identify itself. After all, if every command had just a GUID to identify it, and all the commands that belonged to an add-on had the same GUID, an add-on wouldn't be able to tell the difference between, for instance, the New File command and the New Project command. To disambiguate commands that all have the same GUID, a number, or ID, is assigned to each command in that group. An add-on is responsible for its own commands, so an ID can be assigned without conflicting with commands from a different add-on because the GUID for each add-on is different. When combined, this GUID and ID pair uniquely identifies each individual command.

Note

A command in Visual Studio exists independently of any user interface elements (such as menu items) for that command. Commands can be created and destroyed, and a user interface element might have never been created for that command. But the opposite won't happen—a user interface element can't be created without having a corresponding command.

Locating Commands

In Visual Studio, all the commands that a user can issue are represented in the object model by a Command object, and the Commands collection contains a list of these objects. As do other collection objects, Command objects allow the use of standard enumeration constructs such as the keywords foreach in Visual C# or For Each in Visual Basic. Using these keywords, we can create a macro to walk the list of all Command objects:

Sub WalkCommands()
    Dim cmd As EnvDTE.Command
    For Each cmd In DTE.Commands
      'use the EnvDTE.Command object here
    Next
End Sub

The Command collection's Item method works a bit differently from the Item methods of other collection objects. Commands.Item accepts as a parameter the familiar numerical index, but it also accepts an additional optional argument. If you're using the numerical indexing method, you should set the second argument to -1. This method has an additional argument because, as mentioned earlier, a GUID and ID pair is used to uniquely identify a command. The GUID, in string format, is passed as the first argument, and the ID of the command is passed as the second argument when you're using the GUID and ID to index the Commands collection. The following macro demonstrates finding the command for opening a file by using the GUID and ID pair:

Sub FindFileOpenCommand()
    Dim cmd As EnvDTE.Command
    cmd = DTE.Commands.Item("{5EFC7975-14BC-11CF-9B2B-00AA00573819}", 222)
End Sub

As you can see, code like this can be complicated to write because you need to find and learn the GUID and ID for every command (which would be hard to do because there can be thousands of them), and then you must type this pair correctly every time, which can be a source of programming errors. To help with finding a Command object, the Commands.Item method accepts another format for indexing the collection, which is easier to remember: the name of a command.

Command Names

Remembering the GUID and ID for every command can be a huge waste of brainpower, so Visual Studio defines an easier-to-remember textual representation for most commands. These names follow a general pattern: the text of the top-level menu on which the primary user interface element for the command is located followed by a period, the text of all submenus combined followed by a period, and finally the text of the menu item. Any non-alphanumeric characters (except for the period separators and underscores) are then removed from this string. So to use the earlier example of finding the Command object for the open file command and combine it with our newly found way of using a command name, a macro such as the following results:

Sub FindFileOpenCommandByName()
    Dim command As EnvDTE.Command
    command = DTE.Commands.Item("File.OpenFile")
End Sub

To find the GUID and ID pair of a command, you can use the GUID and ID properties of the Command object. We used these two properties to find the GUID and ID pair used in the FindFileOpenCommand macro shown earlier. This is the macro we used to find them:

Sub FindGuidIDPair()
    Dim guid As String
    Dim id As Integer
    Dim command As EnvDTE.Command
    command = DTE.Commands.Item("File.OpenFile")
    guid = command.Guid
    id = command.ID
    MsgBox(guid + ", " + id.ToString())
End Sub

You can see the entire list of all available commands from within the Options dialog box on the Environment | Keyboard page. You can also use the object model to find available command names. We'll do this with the EnvDTE.Commands collection in this example macro:

Sub CreateCommandList()
    Dim command As EnvDTE.Command
    Dim output As New OutputWindowPaneEx(DTE, "Create Command List")

    For Each command In DTE.Commands
        If (command.Name <> Nothing) Then
            output.WriteLine(command.Name)
        End If
    Next
End Sub

When the macro is run, it places into the Output window the name of each command. If you examine the macro closely enough, you'll notice a special check to verify that the name of the command isn't set to Nothing. This check is done because if a command doesn't have a name set, it returns Nothing if it's using Visual Basic or null if it's using C#. Commands that don't have a name are usually used internally by Visual Studio for private communication, and the user generally shouldn't call them. We advise you not to use these commands because they can lead to unpredictable results.

Executing Commands

The purpose of a command is to provide a way for the user to direct Visual Studio to perform some action. Commands can be invoked in a number of ways, the most common of which is for the user to choose a menu item or click a toolbar button. But commands can also be run in other ways. For example, if you write a macro that conforms to the standard macro notation (it is defined as public, doesn't return a value, and takes no arguments unless the arguments are optional strings), the macros facility detects that macro and creates a command for it. Double-clicking that macro in the Macro Explorer window executes the command associated with that macro, which is handled by the Macros editor. A third way to run a command is to use the DTE.ExecuteCommand method. This method runs a command, given by name, as if the user had chosen the menu item for that command.

To run our File.OpenFile command by using the ExecuteCommand method, we would write code like this:

Sub RunFileOpenCommand()
    DTE.ExecuteCommand("File.OpenFile")
End Sub

When a call is made to the ExecuteCommand method, execution of the macro or add-in waits until the command finishes executing.

A final approach, which is useful for the power user, is to type the name of the command into the Command Window. The Command Window is a text-based window in which you type the names of commands; when the user presses the Enter key, the command is run.

The command name that you type into the Command Window is the same name that is returned from the Command.Name property, and it can be passed to the ExecuteCommand method.

Creating Macro Commands

As mentioned before, macros that follow a special format are automatically turned into commands, and these macro commands are given a named counterpart. The name of a macro command is calculated by combining the string Macros, the name of the macro project, the name of the module or class the macro is implemented in, and finally the name of the macro with each portion separated by a period. Using this format, the TurnOnLineNumbers command in the Samples macro project that is installed with Visual Studio takes on the name Macros.Samples.Utilities.TurnOnLineNumbers. You can enter this name in the Command Window or call it from another macro, like so:

Sub RunCommand()
    DTE.ExecuteCommand("Macros.Samples.Utilities.TurnOnLineNumbers")
End Sub

Creating an Add-In Command

Now that you know how commands are named, found, and run, it's time to see how you can create your own commands. As we saw earlier, when a command built into Visual Studio is invoked, the add-on program for that command is located because of the GUID assigned to the command, and it is asked to handle the command invocation. Likewise, commands that you create need a target that handles the command invocation. Commands can be dynamically created and removed, but creating them requires that an add-in be associated with the new command so Visual Studio can find and use that add-in as the target. The method to create a command, AddNamedCommand2, can be found on the Commands2 collection object. Here is its signature:

public EnvDTE.Command AddNamedCommand2(EnvDTE.AddIn AddInInstance,
    string Name, string ButtonText, string Tooltip, bool MSOButton,
    int Bitmap = 0, ref object[] ContextUIGUIDs,
    int vsCommandStatusValue = 3, int CommandStyleFlags = 3,
    EnvDTE80.vsCommandControlType ControlType = 2)

and here are the arguments:

  • AddInInstance The AddIn object that will act as the command invocation target.

  • Name The name of the command. The name can contain only alphanumeric characters and the underscore character.

  • ButtonText The text that is displayed on any user interface elements, such as buttons, for the command when placed on menus or command bars.

  • ToolTip Descriptive text providing users with information about the command.

  • MSOButton True if the bitmap to display on user interface elements for this command should use the predefined command bar button graphics. If False, the graphic for the button is retrieved from the satellite DLL that is associated with the add-in.

  • Bitmap If the MSOButton argument is True, the value passed to the MSOButton parameter is the index of the predefined command bar button graphic. See the HTML page in the CommandUIBmps folder included with the book's sample files for a listing of available images. If MSOButton is False, the MSOButton parameter is the resource identifier of the bitmap picture in the satellite DLL.

  • ContextUIGUIDs Visual Studio defines a list of GUIDs that, as Visual Studio enters and exits a particular state, such as entering and exiting debugging mode, it marks as active and inactive. Some of these GUIDs are listed in the EnvDTE80.ContextGuids class. When a particular GUID becomes active and that GUID is passed to AddNamedCommand2 through this parameter, the command will become visible. For example, suppose you have a command that helps users debug their code. You could pass the context GUID vsContextGuidDebugging, and when the user starts debugging, your command will use the default state passed for the next parameter, vsCommandStatusValue. If you do not have any context GUIDs for your command, an empty array of type System.Object should be passed for this value, and the state passed to vsCommandStatusValue will always be applied.

  • vsCommandStatusValue This is the default availability state of the button. If the add-in that handles the command invocation has not yet been loaded, rather than forcing the add-in to load to find how the command should be displayed, this argument provides a default availability state. This argument value is used in place of the value returned through the StatusOption argument of the QueryStatus method on the IDTCommandTarget interface, which we'll discuss later in this chapter.

  • CommandStyleFlags User interface elements for the new command can show just a bitmap (as the items on the standard toolbar do), just the name of the text (as the items on the menu bar do), or both. This parameter controls the appearance of the user interface element and is a value from the vsCommandStyle enumeration.

  • ControlType When the command is created, no user interface element—such as a menu item, combo box, or most recently used (MRU) list—is created for that command. But if you intend to create a UI element for the command, you need to declare which kind of element will be created. This declaration is done through this parameter.

When called, the AddNamedCommand2 method adds an item to the internal list of commands maintained by Visual Studio. The full name of the command, which you can use in the Command Window or as an argument to the ExecuteCommand method, is constructed by taking the fully qualified name of the class implementing the add-in (in the form of Namespace.Class) and concatenating a period, followed by the value of the Name parameter. So, for example, if the name you provide to the AddNamedCommand2 method is MyCommand and the Namespace.ClassName of the add-in is MyAddin.Connect, the name of the command that's created is MyAddin.Connect.MyCommand.

All commands added with this method also have a GUID and ID pair assigned to them. The GUID that is used for all commands created with AddNamedCommand2 is defined by the constant EnvDTE.Constants.vsAddInCmdGroup; the ID value starts at the index 1 for the first call to AddNamedCommand2, and depending on the value passed to the ControlType parameter, it is incremented by anywhere from 1 to 25 values every time the AddNamedCommand2 method is called.

Handling a Command Invocation

With a newly created command, our code now needs to provide a way for Visual Studio to call back to the add-in to let it know when the command is invoked. Usually, when an add-in or macro wants to be informed when the user has performed an action, an event connection is made. But command handlers work a bit differently: rather than connecting to an event source, your add-in must implement a specific interface. The reason for not using events is simple. When an add-in command is invoked, if the add-in that handles that command hasn't been loaded, the code for the add-in is loaded into memory and run by calling the OnConnection and other appropriate IDTExtensibility2 methods, just as if you were to go into the Add-in Manager dialog box and select the check box for that add-in. Because the add-in is demand-loaded (loaded when the command is run), code within that add-in could not have been run to connect to an event handler.

The interface to handle command invocations, named IDTCommandTarget, is modeled on the IOleCommandTarget interface of the Win32 SDK, but IDTCommandTarget has been changed to be easier to use with languages such as C# or Visual Basic. This is its signature:

public interface IDTCommandTarget
{
    public void Exec(string CmdName,
        EnvDTE.vsCommandExecOption ExecuteOption, ref object VariantIn,
        ref object VariantOut, ref bool Handled);

    public void QueryStatus(string CmdName,
        EnvDTE.vsCommandStatusTextWanted NeededText,
        ref EnvDTE.vsCommandStatus StatusOption,
        ref object CommandText);
}

When these two methods are called and which values are passed to them depend on the type of command that you are adding to Visual Studio. The simplest case is when you pass the value vsCommandControlTypeButton for the ControlType parameter for AddNamedCommand2, and we will first discuss these two methods in terms of this command type. When invoked, all commands that your add-in creates are dispatched through this interface, particularly through the Exec method. The Exec method has the following arguments:

  • CmdName The full name of the command. Your add-in should do a case-sensitive compare on this string to determine which command is being asked to run because all commands that the add-in creates are sent to this method for handling.

  • vsCommandExecOption For most situations, the value passed to this parameter is the vsCommandExecOptionDoDefault enumeration value, informing your add-in that it should do the work defined for that command.

  • VariantIn As you'll see later in this chapter, commands can be passed data. If any data is passed to your command, they are passed through this argument.

  • VariantOut This argument is used to pass data from your add-in to the caller.

  • Handled This argument allows your add-in to pass back data to Visual Studio, signaling whether your add-in handled the command. If a true value is returned, it is assumed that no further processing for the command is necessary. If this value is set to false on return, Visual Studio continues searching for a handler for the command. The search should fail because no other command handler will accept the same GUID and ID pair for the command your add-in has created.

Command State

A command and its user interface don't always need to be enabled and available to the user. For example, your add-in's command might be available only when a text editor is the currently active window. You can control whether your command is enabled, disabled, or in the latched state (which means a check mark is drawn next to the button if it is a menu item or appears with a box drawn around it if it is on a toolbar). You control this state by using the QueryStatus method of the IDTCommandTarget interface. If your add-in hasn't yet been loaded, the default status, or value passed as the last argument of AddNamedCommand, is used to control the default behavior. However, once you've loaded the add-in—by executing the command or manually through the Add-in Manager dialog box—QueryStatus is called to determine the state. The QueryStatus method has the following arguments:

  • CmdName This argument has the same meaning as the CmdName argument passed to the Exec method of the IDTCommandTarget interface.

  • NeededText This parameter is always vsCommandStatusTextWantedNone. Your add-in should always verify that this value is passed because the other values are reserved for future versions of Visual Studio.

  • StatusOption Your add-in should fill in this parameter, which lets Visual Studio know whether the add-in command is supported (vsCommandStatusSupported) or unsupported (vsCommandStatusUnsupported), whether the command is enabled and can be called (vsCommandStatusEnabled), whether the command user interface can't be seen (vsCommandStatusInvisible), or whether the user interface is drawn in the selected state (vsCommandStatusLatched). You can logically OR these values together to create the current status of the command and pass it back through this argument.

  • CommandText This value currently isn't used by Visual Studio and shouldn't be modified.

Periodically, such as when the focus changes from one window to another or when a menu is displayed that contains an add-in command, Visual Studio calls QueryStatus for that command to ensure that the user interface is synchronized with the command state. It is important to keep the code that implements QueryStatus as efficient as possible; otherwise, the user interface might become sluggish. Suppose you create a command that queries the currently active file's attributes, and the state of a command depends on those file attributes. If the file is on the local disk, calling a method to retrieve the attributes of a file is a relatively fast operation. But if the file is on a network share, that operation can take awhile to perform—especially if the network is temporarily unavailable. A user who has to wait for a command to update itself because he or she showed the menu containing your command would be much happier if the command were always enabled and he or she would receive an error message when the command was invoked.

MRU Button Commands

If you were to choose File | Recent Files, you will see a list of files that you have recently opened. This list of files is implemented using a most recently used menu item list, or MRU button list, and is created with a command type of vsCommandControlTypeMRUButton. An MRU button list allows you to create many related commands with one call to the AddNamedCommand2 method. When UI for these commands are added to a menu, multiple menu buttons are created and grouped together. When this command type is used, Visual Studio will create 25 separate commands rather than just the one it would create when the command type is vsCommandControlTypeButton. If the name of the command supplied to the AddNamedCommand2 method is, for example, MRUButton, and the full name of the class implementing IDTCommandTarget is MyAddin.Connect, then AddNamedCommand2 will create commands named MyAddin.Connect.MRUButton, MyAddin.Connect.MRUButton_1, MyAddin.Connect.MRUButton_2, and so on to MyAddin.Connect.MRUButton_24. This allows you to create an MRU list of 25 separate items.

For this command type, the QueryStatus method is used not only to retrieve the status of the command but also to retrieve the text of the menu item. If your add-in has only four separate MRU items, for the commands MyAddin.Connect.MRUButton, MyAddin.Connect.MRUButton_1, MyAddin.Connect.MRUButton_2, and MyAddin.Connect.MRUButton_3, your query status method returns as the value for status vsCommandStatus.vsCommandStatusSupported. For these values, the commandStatus will also not be null/Nothing, and it is your opportunity to set the text displayed for the menu item. MRU items can have text that changes frequently because (such as in the list of open files) the user will open, close, and remove files quite often, and you will want to keep that list as up-to-date as possible. Because, for this example, you want to display only four MRU menu items, the QueryStatus method should return vsCommandStatus.vsCommandStatusUnsupported for MyAddin.Connect.MRUButton_4 through MyAddin.Connect.MRUButton_24. This bit of code shows how to implement the QueryStatus method for an MRU button list. It maintains a list of four items, which is used for the text of the four menu items. When executed, the code will look at the command name, and if the name is out of the acceptable range, vsCommandStatusUnsupported is passed back to the caller. If the command name is in the appropriate range, it sets the commandText parameter to a string that will be displayed on the menu item for that MRU item, sets the status parameter to a value indicating the command is available, and then returns.

string []MRUItemNames = new string[]
  {"Item 1", "Item 2", "Item 3", "Item 4" };
void QueryStatus(string commandName,
    vsCommandStatusTextWanted neededText,
    ref vsCommandStatus status, ref object commandText)
{
if (neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
    {
        int index = 0;
        string rootCommandName = "MyAddin.Connect.MRUButton";
        if (commandName.StartsWith(rootCommandName))
        {
               //Check for the main command of the group
            if ("MyAddin.Connect.MRUButton" == commandName)
            {
                index = 0;
            }
            else //Command is of the form MyAddin.Connect.MRUButton_*
            {
                index = int.Parse(commandName.Substring(rootCommandName.Length + 1));
            }
            if (index > 3)
              //If the command index is out of the
              //range of MRU items supported...
            { //then the command is should not show
                status = vsCommandStatus.vsCommandStatusUnsupported;
                return;
            }
            else
            { //then show the command, and set the text of the item
                status = (vsCommandStatus)
                  vsCommandStatus.vsCommandStatusSupported |
                  vsCommandStatus.vsCommandStatusEnabled;
                commandText = MRUItemNames[index];
                return;
            }
        }

    }
}

Visual Studio may also call the QueryStatus method just to retrieve the text on the menu item and not require the status of the command. In this case, the neededText parameter will be set to the value vsCommandStatusTextWantedName. You can change the aforementioned QueryStatus to also check for this value, and because both status and commandText are set (setting one value when it is not needed does not cause any side effects—Visual Studio just ignores it) in this code, you will handle both cases:

void QueryStatus(string commandName,
  vsCommandStatusTextWanted neededText,
  ref vsCommandStatus status, ref object commandText)
{
if ((neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone) ||
    (neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedName))
    {
        //TODO: Code to handle the QueryStatus
    }
}

The Exec method for this control type is very much like the Exec method for a vsCommandTypeButton command type, except that you will have multiple, similarly named items being passed to the Exec method. Code to find which item was executed will look similar to that in the QueryStatus method, except, rather than returning the text and status of the command, you will perform the appropriate action for that command:

void Exec(string commandName,
      vsCommandExecOption executeOption,
      ref object varIn, ref object varOut, ref bool handled)
{
    string rootCommandName = "MyAddin.Connect.MRUButton";
    handled = false;
    if (executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
    {
        if (commandName == rootCommandName)
        {
            //Do the operation for MRUItemNames[0]
            handled = true;
            return;
        }
        else if (commandName.StartsWith(rootCommandName))
        {
            int index = int.Parse(commandName.Substring(rootCommandName.Length + 1));
            //Do the operation for MRUItemNames[index]
            handled = true;
            return;
        }
    }
}

Drop-Down Combo Boxes and MRU Combo Boxes

These command types are used to implement a combo box on a command bar. The difference between the two command types is in how the user interacts with them. An MRU combo box lets the user enter new text and also click the arrow to the right of the drop-down combo box to select items that were entered in the past. This is similar to the find combo box on the standard command bar. A drop-down combo box is more restrictive; it allows the user to select only items that you choose from within your add-in code, and the user cannot enter new items. This type of combo box is similar to the Solution Configuration drop-down.

The items in a drop-down combo box are filled in through a call to the Exec method. When the command type is a vsCommandControlTypeDropDownCombo, the AddNamedCommand2 method will create two separate commands. One command is used to retrieve the text that is shown within the main portion of the combo box, indicating the currently selected item. This command is also used to notify your code that the user has executed the command, meaning that the when the user selects an item from within the combo box, the Exec method is called with the text of the item that was selected. The second command that is created is used to retrieve the text of the items that are shown within the drop-down portion of the combo box, listing the possible items that the user can select. When the AddNamedCommand2 method is called, the fully qualified name of the class implementing the add-in in the form of Namespace.ClassName is combined with the name of the command you supply. So if the class name is MyAddin.Connect and the command name is DropDownCombo, the resulting name of the first command that is created is MyAddin. Connect.DropDownCombo. The second command has this same name, except the text "1" is appended, making the command name MyAddin.Connect.DropDownCombo_1. When the method Exec is called with a command name of MyAddin.Connect.DropDownCombo, you will need to check the values of the parameters varIn and varOut to determine if the list of items to fill in the drop-down portion is being asked for, or if a selection was made within the drop-down. If the value of varIn is not null/Nothing, and if the value it contains is a string, a selection was made within the drop-down. However, if the varOut value is not null/Nothing, your add-in is being queried for the currently selected item text to show in the drop-down, and you should set it to a string. Here is an example of the code implementing this first command:

void Exec(string commandName,
  vsCommandExecOption executeOption,
  ref object varIn, ref object varOut, ref bool handled)
{
    if (commandName == "MyAddin.Connect.DropDownCombo")
    {
        if ((varIn != null) && (varIn is string))
        {
            //The command was executed, retrive the selected text.
            string selectedText = varIn as string;
            //Perform some operation on the selected text.
        }
        else
        {
            //The selected text is being asked for, return that here.
            varOut = "Item 1";
        }
        handled = true;
        return;
    }
}

If the Exec method is called with the command name of the second command, the varOut value will not be null/Nothing, and you need to pass back an array containing strings of items to show. This code will fill in the drop-down portion with four items:

if (commandName == "MyAddin.Connect.DropDownCombo_1")
{
    //Set the list of items the user can select.
    varOut = new string[] { "Item 1", "Item 2", "Item 3", "Item 4" };

    handled = true;
    return;
}

The items in an MRU combo box are not filled in by calling the add-in; rather, the items are filled in by the user. The only code that you need to write to handle this command type, which is vsCommandControlTypeMRUCombo, is the QueryStatus code to indicate whether the command is enabled and the Exec code to handle selection within the drop-down. For the Exec method, the varIn parameter contains the text of the selected item when a selection is made. When Visual Studio closes, it will automatically save all the items that the user entered into the MRU combo box, and then it will restore them the next time Visual Studio is started.

Located within the samples that accompany this book, the add-in sample named CommandTypes demonstrates how to use each of these command types.

Programmatically Determining Command State

At times, you might need to programmatically determine whether a command is enabled and can be invoked, such as when you want to invoke a command by using DTE.ExecuteCommand. All commands, whether a macro command, one created by an add-in, or one built into Visual Studio, support a QueryStatus method. When you invoke the DTE.ExecuteCommand but the command isn't enabled because the QueryStatus method returned a value indicating that it isn't currently available, you'll get an exception if you're using a language supported by the .NET Framework.

To check whether a command is enabled and thus avoid this error condition, you can use the Command.IsAvailable property. For example, to make sure that the Build.BuildSolution command can be called before you invoke it, you can use the following code:

Sub CheckAvailability()
    If (DTE.Commands.Item("Build.BuildSolution").IsAvailable = True) Then
        DTE.ExecuteCommand("Build.BuildSolution")
    End If
End Sub

How an Add-In Command Handler Is Found

When a user invokes your command, Visual Studio needs to know which add-in handles that command so it can call the methods of the IDTCommandTarget interface. It first inspects the command name; as noted earlier, the first part of the full command name is the Namespace. ClassName of the add-in, and the remainder is the value passed for the Name parameter of the AddNamedCommand method. To locate the add-in, Visual Studio extracts the Namespace. ClassName from the command name and then checks the add-in corresponding to that Namespace.ClassName to see whether it's loaded. If it isn't, it is told to load. Visual Studio looks for the IDTCommandTarget interface (which must be implemented on the same object that implements IDTExtensibility2) on the add-in object instance, and then it calls the Exec method, passing the name of the command as the first parameter.

If, during this process, the add-in can't be found, the user is presented with the message box shown in Figure 7-1.

The message box displayed by Visual Studio when a command's add-in doesn't load

Figure 7-1. The message box displayed by Visual Studio when a command's add-in doesn't load

If the user clicks the Yes button, the command is removed using the Command.Delete method, and any user interface elements for that command are removed. If the add-in is loaded but the IDTCommandTarget interface can't be found on the add-in object, the command is treated as if the QueryStatus method had returned the vsCommandStatusUnsupported flag.

The Command User Interface

Visual Studio borrows its toolbar and menu system from the Microsoft Office suite of applications. The command bars provide a common user interface experience across all of the Office applications as well as Visual Studio. Because the command bars also support an object model, these applications also share a common programming model for accessing the command bar structure.

The main point of access to the command bar objects is through the DTE. CommandBars property. This property returns a System.Object type, which can be converted into a Microsoft.VisualStudio.CommandBars.CommandBars object, which is defined in the assembly Microsoft.VisualStudio.CommandBars.dll. The following macro code demonstrates retrieving this object:

Sub GetCommandBars()
    Dim commandBars As Microsoft.VisualStudio.CommandBars.CommandBars
    commandBars = DTE.CommandBars
End Sub

The Command Bar Object Model

The command bar object model is arranged in a treelike hierarchy, in the same way as the Visual Studio object model. At the top of this tree is a collection of Microsoft.VisualStudio.CommandBars.CommandBar objects that includes all the command bars, shortcut menus, and the main menu bar. Each command bar contains a collection of controls that have the type Microsoft.VisualStudio.CommandBars.CommandBarControl. Once a CommandBarControl is retrieved, it can be converted into one of a number of different types. One type, a CommandBarButton, is any menu item on a command bar that the user can click to perform an action; this is analogous to executing a Visual Studio command. To get to a CommandBarButton object, a cast must be performed from the CommandBarControl object:

Sub GetCommandBarButton()
    Dim commandBarBtn As Microsoft.VisualStudio.CommandBars.CommandBarButton
    Dim commandBarCtl As Microsoft.VisualStudio.CommandBars.CommandBarControl
    Dim commandBarCtls As Microsoft.VisualStudio.CommandBars.CommandBarControls

    'Find the View command bar
    commandBarCtls = DTE.CommandBars.Item("View").Controls
    'Retrieve the first control on the menu
    commandBarCtl = commandBarCtls.Item(1)
    'Convert the CommandBarControl to a CommandBarButton object
    commandBarBtn = CType(commandBarCtl, _
        Microsoft.VisualStudio.CommandBars.CommandBarButton)
    MsgBox(commandBarBtn.Caption)
End Sub

The object returned from the Controls collection can be converted into a CommandBarPopup if the item is the root node of a submenu. An example of this is the New item on the File menu; when the user holds the mouse cursor over this menu, a submenu appears. You can also retrieve a CommandBarPopup when the item is on a split-button drop-down menu, such as the New Project | New Blank Solution button on the Standard command bar:

Sub GetCommandBarPopup()
    Dim commandBar As Microsoft.VisualStudio.CommandBars.CommandBar
    Dim cmdBarControl As Microsoft.VisualStudio.CommandBars.CommandBarControl
    Dim cmdBarPopup As Microsoft.VisualStudio.CommandBars.CommandBarPopup

    'Find the "Standard" command bar
    commandBar = DTE.CommandBars.Item("Standard")
    'Find the first control on the command bar
    cmdBarControl = commandBar.Controls.Item(1)
    'Convert the CommandBarControl to a CommandBarPopup
    cmdBarPopup = CType(cmdBarControl, _
        Microsoft.VisualStudio.CommandBars.CommandBarPopup)
    MsgBox(cmdBarPopup.Controls.Item(1).Caption)
End Sub

A popup menu is itself a command bar. You can't cast directly to a CommandBar object on a popup menu, but this object does contain a CommandBar property, which returns a CommandBar object, which itself has a collection of controls (as you can see in the next-to-last line in the preceding macro code).

The Primary Command Bar

The DTE.CommandBars property returns the collection of all CommandBar objects available within Visual Studio, but the most commonly used command bar is the main menu. Looking at the menu, you can see the File, Edit, and View items as well as a number of additional menu items; all of these are CommandBar objects within the DTE.CommandBars collection. But because there might be multiple items within the collection with the same name, indexing the collection by using the name might not work. For example, there are multiple CommandBar objects with the title View, and you might not always get the one you want if you index the CommandBars collection with the string View. The following macro might return the View command bar for the SQL editor, a deployment project popup menu, or the View menu:

Sub GetView()
    Dim cmdbars As Microsoft.VisualStudio.CommandBars.CommandBars
    Dim commandBar As Microsoft.VisualStudio.CommandBars.CommandBar

    cmdbars = DTE.CommandBars
    commandBar = cmdbars.Item("View")
End Sub

To work around this, you can find the CommandBar object for the menu bar, called MenuBar, and then find the View submenu command bar:

Sub GetMenuCommandBar()
    Dim commandBar As Microsoft.VisualStudio.CommandBars.CommandBar
    Dim cmdBarControl As Microsoft.VisualStudio.CommandBars.CommandBarControl
    Dim cmdBarPopupView As Microsoft.VisualStudio.CommandBars.CommandBarPopup
    Dim cmdBarView As Microsoft.VisualStudio.CommandBars.CommandBar

    'Retrieve the MenuBar command bar
    commandBar = DTE.CommandBars.Item("MenuBar")
    'Find the View menu
    cmdBarControl = commandBar.Controls.Item("View")
    'Convert to a CommandBarPopup
    cmdBarPopupView = CType(cmdBarControl, _
        Microsoft.VisualStudio.CommandBars.CommandBarPopup)
    'Get the CommandBar object for the view menu
    cmdBarView = cmdBarPopupView.CommandBar
    MsgBox(cmdBarView.Name)
End Sub

By default, if the Add-in Wizard generates an add-in and the option is selected to place an item on the Tools menu, code is generated to place a menu item on the Tools menu of the menu bar. If you want to move this command user interface to a different menu, you can simply change the string Tools to a different menu title, but be careful to select the correct menu. It's easy to make the mistake of selecting the wrong command bar, causing the command button to seemingly disappear because it was placed somewhere that you did not expect it to go.

Adding New Command Bar Elements

With a Command object in hand (found by either indexing the Commands collection or adding a new command), and after using the methods described earlier to find the proper command bar, you can add new UI elements to that command bar that invokes your command when clicked. You do this by using the Command.AddControl method. When a control is added, the control type of the command (either passed to the call of AddNamedCommand2 or the type of one of the built-in commands) is used to create the appropriate command type, be that a menu item, an MRU menu item list, or one of the two combo box types. When commands are created, they are persisted to disk and re-created automatically the next time Visual Studio is started. Likewise, when you place a control on a command bar by using the AddControl method, that control and its placement are saved to disk and re-created when Visual Studio is run. The first argument of the AddControl method is the CommandBar object that the button is to be placed on. The second argument defines the numerical position of the control in relation to the other controls on the command bar. (If this value is 1, the control will be the first item on the command bar, and if the value is 2, it will be the second item, and so forth.)

You can hard-code an index to place the control, but the control might not appear where you think it should go in relation to other controls. The reason is that a command bar might have one or more separators (or lines drawn between two controls) that divide controls into logical groups. These groups are also controls on the command bar, and they should be counted when you calculate the position. Not only are group controls counted as items in the index, but so are controls that are not visible because the value vsCommandStatusInvisible is returned from your QueryStatus method. If the control to be added should be placed at the bottom or end of the command bar, you can use the Controls.Count property to determine the final position:

Sub AddControl()
    Dim command As EnvDTE.Command
    Dim commandBar As Microsoft.VisualStudio.CommandBars.CommandBar

    'Find the File.OpenFile command
    command = DTE.Commands.Item("File.OpenFile")
    'Find the Tools CommandBar
    commandBar = DTE.CommandBars.Item("Tools")
    'Add a control to the Tools menu that when
    ' clicked will invoke the File.OpenFile command
    command.AddControl(commandBar, commandBar.Controls.Count + 1)
  End Sub

Note that the index used doesn't fix a control to a particular position. If you add a control to position 1 and a second control is added to position 1, the first control is pushed into the second position.

At times, it might make sense to create a new command bar to place your buttons on because the default set of command bars doesn't suit your needs. The command bar object model allows you to create new command bars, but creating one in this way might not achieve the desired effects. Command bars created in this way are created in a temporary state, which means that when you exit and restart Visual Studio, the command bar will have been destroyed. Because the button user interface for commands persists across instances, you'll want your command bars to also persist across instances. The Visual Studio object model lets you do this by using the Commands.AddCommandBar method, which has this signature:

object AddCommandBar(string Name, EnvDTE.vsCommandBarType Type, _
  Microsoft.VisualStudio.CommandBars.CommandBar
  CommandBarParent = null, int Position = 1)

This method has the following arguments:

  • Name The caption to display on the command bar.

  • Type A value from the vsCommandBarType enumeration. If the value is vsCommandBarTypeToolbar, a command bar is created that can be docked to the top, left, bottom, or right of the Visual Studio window. If the value is vsCommandBarTypeMenu, the command bar is added as a submenu to another command bar. If the value is vsCommandBarTypePopup, a shortcut menu is created.

  • CommandBarParent If the value passed for the Type parameter is vsCommandBarTypeToolbar or vsCommandBarTypePopup, this value should be null or Nothing (depending on the language used). If the value passed to the Type parameter is vsCommandBarTypeMenu, the new menu should be rooted on the command bar object.

  • Position This value is necessary only if the Type parameter is set to vsCommandBarTypeMenu. It defines the location on the parent command bar where the new menu command is placed. It has the same meaning as the Position parameter of the AddControl method.

How the newly created command bar is shown to the user depends on the type of command bar that's created. If the command bar type is a new menu, the menu item is hidden from the user until the command bar for that menu item is populated with buttons. If the command bar created is a new toolbar, the Visible property of the returned CommandBar object should be set to True. If a popup menu is created, you can show the menu to the user by using the CommandBar.ShowPopup method, which takes two arguments, the x and y coordinates of the top left of where the popup menu should appear.

Using Custom Bitmaps

Visual Studio has a number of predefined bitmaps that you can place on menu items and command bar buttons, but they might not always meet your needs. To use your own bitmap for the image on a button, you must create for your add-in a satellite DLL that contains the bitmap as a resource, and then change the call to Commands2.AddNamedCommand2 so Visual Studio can find your bitmap. First, you should set the AddNamedCommand2 method's MSOButton parameter to false to tell Visual Studio that the bitmap isn't among the default, built-in pictures but is in the satellite DLL. Second, you should change the Bitmap parameter to the resource name of the bitmap in your satellite DLL; however, unlike other places where resource names can use alphanumeric values for the resource names, this method accepts only a number in the form of a string for the resource name. The bitmap must be in a specific format to be usable by Visual Studio. It must be 16 pixels high and 16 pixels wide, and it must be saved so that it has either 4-bit color (16 colors) or 32-bit color (4,294,967,296 colors). Visual Studio can also draw the picture so that a portion of it shows as transparent, causing the command bar background to bleed through. To enable this, you must make the transparent area have the RGB (red, green, blue) color value of 0, 254, 0. Note that this color isn't the lime green color displayed in the color palette of the Visual Studio image editor or the Windows Paint application, and you will need to use the palette color manipulation features built into those tools.

Once you have created the .resx file and the bitmap within that file, you then need to generate the satellite DLL. I like to use the command line console rather than Visual Studio to generate the DLL. This is purely a matter of personal preference; you can use Visual Studio to build it for you. Using the integrated resource editor makes editing the .resx file much easier.

The sample CommandTypes, which was used earlier to demonstrate creating commands that implement combo-boxes and MRU menu item lists, also uses a custom bitmap with transparency for some of the command menu items. The call to AddNamedCommand2 has been modified as described earlier; all that is left is to generate the satellite DLL. In the Localization folder, along with the CommandTypes sample is a batch file, MakeSatelliteDLL. bat, which will generate the satellite DLLs. This batch file repeats the following bit of code many times, but for a selection of different languages:

Resgen CommandTypes.en-US.resx
Al.exe /t:lib /embed:CommandTypes.en-US.resources
  /culture:en-US /out:CommandTypes.resources.dll
md ...CommandTypesinen-US
copy CommandTypes.resources.dll ...CommandTypesinen-US

The first line of this code takes a resource file, in this case the resources for the U.S. English culture, and creates a .resources file with the name CommandTypes-en-US.resources. Next, the assembly linker tool (Al.exe) is called to build a library DLL with the U.S. English resources embedded within it, and it names that file CommandTypes.resources.dll. The final two lines simply copy the file into a folder named en-US that is located in the same directory as the add-in DLL. When you call AddNamedCommand2 from your add-in and the MSOButton parameter is set to false, Visual Studio will search for the satellite DLL assembly, load it, find the specified bitmap resource, and then apply it for the command UI that is created for that command. You do not need to modify the .addin file to specify the satellite DLL; Visual Studio will find it by using the .NET Framework's System.Reflection.Assembly.GetSatelliteAssembly method.

Regenerating Commands and Their User Interface

As you are developing your add-in, you may need to add, remove, or modify the attributes of the commands that you are creating. If you were to run the add-in wizard and select the option to create a Tools menu command, the appropriate code to generate a menu item is generated. After running the wizard-generated add-in, if you select the Tools menu, you will see the newly created menu item. But suppose you needed to modify this code to create a combo box instead. If you were to make the appropriate modifications to the code, compile, and then from the Windows Start menu, start a new instance of Visual Studio, the item on the Tools menu will still be a menu item. Why is this?

When the code for a wizard is generated, the appropriate tag is placed into the .addin XML file indicating that the add-in wants to generate a command. When Visual Studio runs, it notices this tag, the add-in is loaded, and the UI setup block of code (the portion of code that checks for the ext_cm_UISetup flag in the connectMode parameter of OnConnection) is run. But this portion of code is run only once; Visual Studio remembers if the add-in's UI setup block has been run, and it will not go through these steps again. Any commands and UI for those commands are persisted across instances of Visual Studio. This is a huge performance benefit, in that your add-in is not loaded and executed every time Visual Studio is run. If, after making the code changes to change the UI type, you were to run the new add-in again from within Visual Studio (by pressing F5 or Ctrl+F5, not from the start menu in Windows), the command will be changed. This happens because the wizard-generated code modifies how the wizard is run when launched from within Visual Studio. If you were to open the properties window for your add-in project and inspect the Debug tab of the properties window, you would notice that a command-line argument named /resetaddin is used. This switch directs Visual Studio to find all the commands and command UI owned by the specified add-in and remove them before starting. Visual Studio will then reload the add-in and execute the UI setup block of the add-in again, causing your commands to be regenerated.

The /resetaddin command-line argument requires a value when it is used. The first possible value is the fully qualified name (in the form of the Namespace.ClassName) of the add-in to reset. The second possible value is the asterisk character (*). When this value is used, all commands and their UI for all add-ins are removed. This gives you a way of cleaning up all add-ins at once. You can also use the /resetaddin switch if you are trying to delete an add-in from the system (such as from an uninstall program). First, you will need to delete the .addin file and any .dll files placed on disk for your add-in. Your uninstall program can then issue the command line devenv /resetaddin Namespace.ClassName /command File.Exit, where Namespace.ClassName is the fully qualified name of the class implementing the add-in. This will force all the commands to be deleted for the add-in, and the /command File.Exit command line switch will force Visual Studio to close when the commands have been deleted. Because the .addin file is no longer present, the commands for that add-in will not be re-created.

Looking Ahead

In the next chapter, we'll focus on using the object model to create and modify solutions and projects that are loaded into Visual Studio. We'll also look at how to work with those solutions, such as changing how a solution and projects within the solution are compiled into a running program.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset