Dotneteers.net
All for .net, .net for all!

LearnVSXNow! #25 - Advanced VSCT Concepts: Behind Combos

In Part #18 I gave you a very short overview about combo boxes in the context of VSCT files. In that article I promised you to give a detailed overview about declaring and using combo boxes. Nowadays beside LearnVSXNow I’m working on my other project called VSXtra, and I have just finished the first implementation of a concept about working with combo boxes in a new way.

In this article I treat in details how to use combo boxes with Visual Studio Packages and also would like to present you an improved way with VSXtra.

The Visual Studio SDK comes with a sample called “C# Combobox Reference Sample”. I use this sample, because it gives you a great overview about how to use different stereotypes of combo boxes with menu and toolbar definitions. So, if you want to try how combos work, please look after this VS SDK sample and open it with Visual Studio.

Combo box stereotypes

Combo boxes provide you an enhanced form of input by allowing you a combination of hand-typed text selection with picking items from a list. Visual Studio also provides comboboxes on its user interface. The command table file (.vsct) has Combo elements to represent UI of commands with combo boxes instead of buttons. There are four stereotypes of combos; each of them is identified by a type name used in the .vsct file.

—  DropDownCombo: This type does not let the user type into the combo box; they can only pick an item from a list. After selecting an item the string value of the selected element is returned.

—  IndexCombo: This type is the same as a DropDownCombo in that it allows only picking up items from a list. However, an IndexCombo returns the zero-based index of the selected value on the list and not the value itself.

—  MRUCombo: This combo type allows the user to type into the edit box. The history of strings entered is automatically persisted by the IDE on a per-user or per-machine basis for the last 16 items.

—  DynamicCombo: This combo allows the user to type into the edit box or pick from the list. The list of choices is managed dynamically by a command event handler method.

Analyzing the .VSCT file

Open the VS SDK “C# Combo box Reference” sample solution in Visual Studio and have a look at the PkgCmd.vsct file. This file contains the definition of the Combo Box Sample toolbar.

In this part I show you what the structure of the .vsct file is and how the different combo box stereotypes are defined. Here is the full code of the .vsct file (I have omitted the comments):

<?xml version="1.0" encoding="utf-8"?>

<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable"

  xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <Extern href="stdidcmd.h" mce_href="stdidcmd.h"/>

  <Extern href="vsshlids.h" mce_href="vsshlids.h"/>

  <Extern href="msobtnid.h" mce_href="msobtnid.h"/>

 

  <Commands package="guidComboBoxPkg">

    <Menus>

      <Menu guid="guidComboBoxCmdSet" id="MyToolbar" priority="0x0000" type="Toolbar">

        <Parent guid="guidComboBoxCmdSet" id="0"/>

        <Strings>

          <ButtonText>ComboBoxSample</ButtonText>

          <CommandName>Combo Box Sample</CommandName>

        </Strings>

      </Menu>

    </Menus>

 

    <Groups>

      <Group guid="guidComboBoxCmdSet" id="MyToolbarGroup" priority="0xFF45">

        <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>

      </Group>

    </Groups>

 

    <Combos>

      <Combo guid="guidComboBoxCmdSet" id="cmdidMyDropDownCombo" priority="0x0010" type="DropDownCombo"

        defaultWidth="130" idCommandList="cmdidMyDropDownComboGetList">

        <Parent guid="guidComboBoxCmdSet" id="MyToolbarGroup"/>

        <CommandFlag>IconAndText</CommandFlag>

        <CommandFlag>CommandWellOnly</CommandFlag>

        <Strings>

          <MenuText>DropDown Combo: </MenuText>

          <ButtonText>DropDown Combo</ButtonText>

          <ToolTipText>Select String</ToolTipText>

          <CanonicalName>DropDown Combo</CanonicalName>

          <CommandName>DropDown Combo</CommandName>

        </Strings>

      </Combo>

     

      <Combo guid="guidComboBoxCmdSet" id="cmdidMyIndexCombo" priority="0x0010" type="IndexCombo"

        defaultWidth="100" idCommandList="cmdidMyIndexComboGetList">

        <Parent guid="guidComboBoxCmdSet" id="MyToolbarGroup"/>

        <CommandFlag>IconAndText</CommandFlag>

        <CommandFlag>CommandWellOnly</CommandFlag>

        <Strings>

          <ButtonText>Index: </ButtonText>

          <MenuText>Index Combo: </MenuText>

          <ToolTipText>Select Choice</ToolTipText>

          <CommandName>Index Combo</CommandName>

          <CanonicalName>Index Combo</CanonicalName>

        </Strings>

      </Combo>

     

      <Combo guid="guidComboBoxCmdSet" id="cmdidMyMRUCombo" priority="0x0040" type="MRUCombo"

        defaultWidth="300" idCommandList="0">

        <Parent guid="guidComboBoxCmdSet" id="MyToolbarGroup"/>

        <CommandFlag>IconAndText</CommandFlag>

        <CommandFlag>CommandWellOnly</CommandFlag>

        <CommandFlag>NoAutoComplete</CommandFlag>

        <CommandFlag>CaseSensitive</CommandFlag>

        <Strings>

          <ButtonText>MRU: </ButtonText>

          <MenuText>MRU Combo: </MenuText>

          <ToolTipText>Enter String</ToolTipText>

          <CommandName>MRU Combo</CommandName>

          <CanonicalName>MRU Combo</CanonicalName>

        </Strings>

      </Combo>

     

      <Combo guid="guidComboBoxCmdSet" id="cmdidMyDynamicCombo" priority="0x0050" type="DynamicCombo"

        defaultWidth="135" idCommandList="cmdidMyDynamicComboGetList">

        <Parent guid="guidComboBoxCmdSet" id="MyToolbarGroup"/>

        <CommandFlag>IconAndText</CommandFlag>

        <CommandFlag>CommandWellOnly</CommandFlag>

        <Strings>

          <ButtonText>Dynamic: </ButtonText>

          <MenuText>Dynamic Combo: </MenuText>

          <ToolTipText>Enter Zoom Level</ToolTipText>

          <CommandName>Dynamic Combo</CommandName>

          <CanonicalName>Dynamic Combo</CanonicalName>

        </Strings>

      </Combo>

    </Combos>

  </Commands>

 

  <CommandPlacements>

    <CommandPlacement guid="guidComboBoxCmdSet" id="MyToolbarGroup" priority="0x0100">

      <Parent guid="guidComboBoxCmdSet" id="MyToolbar"/>

    </CommandPlacement>

  </CommandPlacements>

 

  <Symbols>

    <GuidSymbol name="guidComboBoxPkg" value="{40d9f297-25fb-4264-99ed-7785f8331c94}" />

    <GuidSymbol name="guidComboBoxCmdSet" value="{04151d39-35b6-4e53-beee-48df3bb8cfe5}">

     

      <IDSymbol name="MyToolbar" value="0x1000"/>

      <IDSymbol name="MyToolbarGroup" value="0x1030"/>

     

      <IDSymbol name="cmdidMyDropDownCombo" value="0x101"/>

      <IDSymbol name="cmdidMyDropDownComboGetList" value="0x102"/>

      <IDSymbol name="cmdidMyIndexCombo" value="0x103"/>

      <IDSymbol name="cmdidMyIndexComboGetList" value="0x104"/>

      <IDSymbol name="cmdidMyMRUCombo" value="0x105"/>

      <IDSymbol name="cmdidMyDynamicCombo" value="0x107"/>

      <IDSymbol name="cmdidMyDynamicComboGetList" value="0x108"/>

    </GuidSymbol>

  </Symbols>

</CommandTable>

Now, let’s see the details of the file. I assume that you are familiar with the basic concepts of the .vsct file. If not, please read the LearnVSXNow! #14 - Basics of the .vsct file article.

Placing the combo box controls onto the toolbar

The Menus element declares only one Menu item representing our toolbar:

<Menus>

  <Menu guid="guidComboBoxCmdSet" id="MyToolbar" priority="0x0000" type="Toolbar">

    <Parent guid="guidComboBoxCmdSet" id="0"/>

    <Strings>

      <ButtonText>ComboBoxSample</ButtonText>

      <CommandName>Combo Box Sample</CommandName>

    </Strings>

  </Menu>

</Menus>

The value “Toolbar” in the type attribute signs that we are declaring a new toolbar. Because the Parent child element under Menu is mandatory we must use it. However, a toolbar does not have a real parent, so as a convention the same guid as for the Menu and the id of 0 is used here.

In the example we user four combo boxes that are logically organized into one group defined by a new Group element:

<Groups>

  <Group guid="guidComboBoxCmdSet" id="MyToolbarGroup" priority="0xFF45">

    <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>

  </Group>

</Groups>

This Group uses actually the Tools menu as its parent! How this group is associated with the toolbar? A CommandPlacement definition use used to build up this assignment:

<CommandPlacements>

  <CommandPlacement guid="guidComboBoxCmdSet" id="MyToolbarGroup" priority="0x0100">

    <Parent guid="guidComboBoxCmdSet" id="MyToolbar"/>

  </CommandPlacement>

</CommandPlacements>

But wait! Actually we added the Group holding combo box definitions both to the Tools menu and to the toolbar frame! Instead, we could have created a simple Group definition like this:

<Groups>

  <Group guid="guidComboBoxCmdSet" id="MyToolbarGroup" priority="0xFF45">

    <Parent guid="guidComboBoxCmdSet" id="MyToolbar"/>

  </Group>

</Groups>

You are right, this simple definition works! The reason why the sample uses the pattern above is customization. By adding any command to the main menu allows command and toolbar customization (the user can organize the commands among menus and toolbars).

Although the combo boxes are added to the main menu with the Parent element of the Group definition and to the toolbar with the CommandPlacement declaration, they are invisible in the main menu due to the CommandWellOnly flag associated with each combo. This command flag instructs the Visual Studio Shell to hide the corresponding items from the main menu but enable their customization.

To see what CommandWellOnly flag does, just comment out one of them. The combo without this flag will be displayed in the main menu as you see in the next figure:

Combo box declarations

The .vsct file contains Combos node to declare combo boxes in its Combo child nodes. Combo declarations are very similar to Button declarations, but those use more attributes and child elements to define combo appearance and behavior. The following extract contains the declaration for a DropDownCombo:

<Combo guid="guidComboBoxCmdSet" id="cmdidMyDropDownCombo"

  priority="0x0010"

  type="DropDownCombo"

  defaultWidth="130"

  idCommandList="cmdidMyDropDownComboGetList">

  <Parent guid="guidComboBoxCmdSet" id="MyToolbarGroup"/>

  <CommandFlag>IconAndText</CommandFlag>

  <CommandFlag>CommandWellOnly</CommandFlag>

  <Strings>

    <MenuText>DropDown Combo: </MenuText>

    <ButtonText>DropDown Combo</ButtonText>

    <ToolTipText>Select String</ToolTipText>

    <CanonicalName>DropDown Combo</CanonicalName>

    <CommandName>DropDown Combo</CommandName>

  </Strings>

</Combo>

Just as for Button elements, guid and id attributes identify the command represented by the combo. The type attribute names the stereotype of the combo box with the values of DropDownCombo, IndexCombo, MRUCombo and DynamicCombo. Except of MRUCombo the other combo box types have lists filled up by package owning the combo box. It is done so that these three types of combos actually have two commands:

—  The “main” command represented by the guid and id attributes is called when the shell is about to get the current selection of the combo or set it to a value.

—  There is a secondary command represented by the guid and idCommandList attributes. This is called when the shell is about to fill up the list belonging to the combo box. Actually, any time when you drop down the list of a combo this command is used.

Unlike buttons, combos can have a suggested width set by the defaultWidth attribute. It is specified in pixels and includes the width of the entire combo box including the text area, icon and label.

You can see in the .vsct file that two CommandFlag elements are used for each combo. The IconAndText flag specifies that both the label and icon for the combo should be displayed. The CommandWellOnly is used to hide the combos from the main menu but to allow user customization.

The Strings element is a container for various strings for the Combo elements. Their semantics is similar to the ones used for buttons.

Using combo boxes in code

In order combo boxes can be used, command handlers should be bound to them. For buttons only a simple command handler is required to respond to the event when the user clicks on the button. However, for combo boxes the situation is a bit more complicated:

The shell must be able to obtain the current value of the combo box and also to set it. So, besides executing an action, the standard command handler is responsible for getting and setting the combo box value. The combo box has to be able to publish the collection of items in the drop-down list part of the combo. This is done by a separate command.

The behavior of combo boxes require special command handler binding and command handler methods have more responsibilities. To understand how these concepts work, let us see some code extracts from the Combo box reference sample.

The command handler binding is done in the overridden Initialize method of the package:

protected override void Initialize()

{

  base.Initialize();

  OleMenuCommandService mcs = GetService(typeof(IMenuCommandService))

    as OleMenuCommandService;

  if (null != mcs)

  {

    // --- Initialize the DropDownCombo

    CommandID menuMyDropDownComboCommandID =

      new CommandID(GuidList.guidComboBoxCmdSet,

      (int)PkgCmdIDList.cmdidMyDropDownCombo);

    OleMenuCommand menuMyDropDownComboCommand =

      new OleMenuCommand(new EventHandler(OnMenuMyDropDownCombo),

      menuMyDropDownComboCommandID);

    menuMyDropDownComboCommand.ParametersDescription = "$";       

    mcs.AddCommand(menuMyDropDownComboCommand);

 

    // --- Initialize the “GetList” command for DropDownCombo

    CommandID menuMyDropDownComboGetListCommandID =

      new CommandID(GuidList.guidComboBoxCmdSet,

      (int)PkgCmdIDList.cmdidMyDropDownComboGetList);

    MenuCommand menuMyDropDownComboGetListCommand =

      new OleMenuCommand(new EventHandler(OnMenuMyDropDownComboGetList),

      menuMyDropDownComboGetListCommandID);

    mcs.AddCommand(menuMyDropDownComboGetListCommand);

 

    // --- Initialize the IndexCombo

    CommandID menuMyIndexComboCommandID =

      new CommandID(GuidList.guidComboBoxCmdSet,

      (int)PkgCmdIDList.cmdidMyIndexCombo);

    OleMenuCommand menuMyIndexComboCommand =

      new OleMenuCommand(new EventHandler(OnMenuMyIndexCombo),

      menuMyIndexComboCommandID);

    menuMyIndexComboCommand.ParametersDescription = "$";         

    mcs.AddCommand(menuMyIndexComboCommand);

 

    // --- Initialize the “GetList” command for IndexCombo

    CommandID menuMyIndexComboGetListCommandID =

      new CommandID(GuidList.guidComboBoxCmdSet,

      (int)PkgCmdIDList.cmdidMyIndexComboGetList);

    MenuCommand menuMyIndexComboGetListCommand =

      new OleMenuCommand(new EventHandler(OnMenuMyIndexComboGetList),

      menuMyIndexComboGetListCommandID);

    mcs.AddCommand(menuMyIndexComboGetListCommand);

 

    // --- Initialize the MRUCombo

    CommandID menuMyMRUComboCommandID =

      new CommandID(GuidList.guidComboBoxCmdSet,

      (int)PkgCmdIDList.cmdidMyMRUCombo);

    OleMenuCommand menuMyMRUComboCommand =

      new OleMenuCommand(new EventHandler(OnMenuMyMRUCombo),

      menuMyMRUComboCommandID);

    menuMyMRUComboCommand.ParametersDescription = "$";       

    mcs.AddCommand(menuMyMRUComboCommand);

 

    // --- Initialize the DynamicCombo

    CommandID menuMyDynamicComboCommandID =

      new CommandID(GuidList.guidComboBoxCmdSet,

      (int)PkgCmdIDList.cmdidMyDynamicCombo);

    OleMenuCommand menuMyDynamicComboCommand =

      new OleMenuCommand(new EventHandler(OnMenuMyDynamicCombo),

      menuMyDynamicComboCommandID);

    menuMyDynamicComboCommand.ParametersDescription = "$";            

    mcs.AddCommand(menuMyDynamicComboCommand);

 

    // --- Initialize the “GetList” command for IndexCombo

    CommandID menuMyDynamicComboGetListCommandID =

      new CommandID(GuidList.guidComboBoxCmdSet,

      (int)PkgCmdIDList.cmdidMyDynamicComboGetList);

    MenuCommand menuMyDynamicComboGetListCommand =

      new OleMenuCommand(new EventHandler(OnMenuMyDynamicComboGetList),

      menuMyDynamicComboGetListCommandID);

    mcs.AddCommand(menuMyDynamicComboGetListCommand);

  }

}

As you see from the code, we use the same steps for binding a handler to combo box command as for button commands. First we create a CommandID instance then an OleMenuCommand instance and set it up with the corresponding command identifier and event handler method(s). As the last step, we add the command to the OleMenuService instance we obtained at the beginning of the method. You can follow in the code how we bind two commands for each combo box stereotypes except MRUCombo.

There is one more interesting piece of code: we set the ParametersDescription property of the OleMenuCommand instances to “$”. This looks a bit esoteric, due to the fact that ParametersDescription is not a well-documented property.

Here I do not explain you all the details, but try to give you a few hints about what this parameter does. Actually, each command available in Visual Studio can be used from command line through the Command window. When using commands from command line, parameters can be passed to the command. The ParametersDescription property is used to tell the shell what kind of parameters are accepted by a command when starting from the command line. The “$” used in this sample tells the shell that the rest of the command line following the command name is the parameter of that command.

You could leave the ParametersDescription property unset, the example would still work.

Creating the standard command handler

The combo box command handler methods have the same signature as any other standard event handler methods:

void EventHandlerMethod(object sender, EventArgs e);

However, for combo boxes the shell passes an OleMenuCmdEventArgs instance for e. This is important: an OleMenuCmdEventArgs has an InValue property (type of System.Object) and an OutValue property (type of System.IntPtr). When the handler method executes and OutValue is not empty (not null), the shell expects the method to retrieve the current value of the combo box in OutValue. Should InValue be not empty, the shell signs the current value of the combo box should be set according to the string provided by InValue. It is not legal that both InValue and OutValue are empty or both are filled with values.

The standard event handler methods for combo boxes should follow this pattern:

private void OnMyComboExec(object sender, EventArgs e)

{

  if (e == EventArgs.Empty)

  {

    // --- We should never get here; EventArgs are required.

    throw (new ArgumentException(Resources.EventArgsRequired));

  }

  OleMenuCmdEventArgs eventArgs = e as OleMenuCmdEventArgs;

  if (eventArgs != null)

  {

    string newChoice = eventArgs.InValue as string;

    IntPtr vOut = eventArgs.OutValue;

    if (vOut != IntPtr.Zero && newChoice != null)

    {

      throw (new ArgumentException(Resources.BothInOutParamsIllegal));

    }

    else if (vOut != IntPtr.Zero)

    {

      // --- The IDE is requesting the current value for the combo

      Marshal.GetNativeVariantForObject(currentValue, vOut);

    }

    else if (newChoice != null)

    {

      // --- New value was selected or typed in, check if it is a valid input

      bool validInput = ... // --- the input here

      if (validInput)

      {

        // --- Store the new value
        // --- Execute the command action

      }

      else

      {

        // --- An invalid input has been specified

        throw (new ArgumentException(Resources.ParamNotValidStringInList));             

      }

    }

    else

    {

      // --- We should never get here

      throw (new ArgumentException(Resources.InOutParamCantBeNULL));

    }

  }

  else

  {

    // --- We should never get here; EventArgs are required.

    throw (new ArgumentException(Resources.EventArgsRequired));

  }

}

The majority of this code deals with parameter checks. When we retrieve the current value of the combo box, we must use the Marshal.GetNativeVariantForObject method to pass back the output value.

When the IDE is about to set the current value, we must check if the value is valid (it can be accepted by our combo). It always depends on the stereotype and semantics of our combo box if we take a value into account as valid or as invalid. The DropDownCombo implements the pattern above in this way:

private string[] dropDownComboChoices = { Resources.Apples, Resources.Oranges,
  Resources.Pears, Resources.Bananas };
private string currentDropDownComboChoice = Resources.Apples; 

private void OnMenuMyDropDownCombo(object sender, EventArgs e)

{

  if (e == EventArgs.Empty)

  {

    throw (new ArgumentException(Resources.EventArgsRequired));      }

    OleMenuCmdEventArgs eventArgs = e as OleMenuCmdEventArgs;

  if (eventArgs != null)

  {

    string newChoice = eventArgs.InValue as string;

    IntPtr vOut = eventArgs.OutValue;

    if (vOut != IntPtr.Zero && newChoice != null)

    {

      throw (new ArgumentException(Resources.BothInOutParamsIllegal));

    }

    else if (vOut != IntPtr.Zero)

    {

      Marshal.GetNativeVariantForObject(this.currentDropDownComboChoice, vOut);

    }

    else if (newChoice != null)

    {

      bool validInput = false;
      int indexInput = -1;
      for (indexInput = 0; indexInput < dropDownComboChoices.Length; indexInput++)
      {
        if (String.Compare(dropDownComboChoices[indexInput], newChoice,
          StringComparison.CurrentCultureIgnoreCase) == 0)
        {
          validInput = true;
          break;
        }
      }
      if (validInput)
      {
        this.currentDropDownComboChoice = dropDownComboChoices[indexInput];
        ShowMessage(Resources.MyDropDownCombo, this.currentDropDownComboChoice);
      }

      else

      {

        throw (new ArgumentException(Resources.ParamNotValidStringInList));         

      }

    }

    else

    {

      throw (new ArgumentException(Resources.InOutParamCantBeNULL));

    }

  }

  else

  {

    throw (new ArgumentException(Resources.EventArgsRequired));

  }

}

We store the available selection values in the dropDownComboChoices array (we look later how the content of this array is passed to the shell). The current selection is stored in the currentDropDownComboChoice string variable. The highlighted code simply checks if the new selection is one of the available values (using case-insensitive comparison). If the new value is acceptable we store it and execute the command.

The IndexCombo uses a bit different approach to set the value, as the following code extract indicates it:

private void OnMenuMyIndexCombo(object sender, EventArgs e)

{

  // --- Some checks omitted for clarity

  OleMenuCmdEventArgs eventArgs = e as OleMenuCmdEventArgs;

  if (eventArgs != null)

  {

    object input = eventArgs.InValue;

    IntPtr vOut = eventArgs.OutValue;

   

    // --- Some checks and current value retrieval is omitted

    if (input != null)

    {

      int newChoice = -1;

      try

      {

        // --- User can type a string argument in command window.

        int index = int.Parse(input.ToString(), CultureInfo.CurrentCulture);

        if (index >= 0 && index < indexComboChoices.Length)

        {

          newChoice = index;

        }

        else

        {

          string errorMessage = string.Format(CultureInfo.CurrentCulture,

            Resources.InvalidIndex, indexComboChoices.Length);

          throw (new ArgumentOutOfRangeException(errorMessage));

        }

      }

      catch (FormatException)

      {

        // --- User typed in a non-numeric value, see if it is one of our items

        for (int i = 0; i < indexComboChoices.Length; i++)

        {

          if (String.Compare(indexComboChoices[i], input.ToString(),

            StringComparison.CurrentCultureIgnoreCase) == 0)

          {

            newChoice = i;

            break;

          }

        }

      }

      catch (OverflowException)

      {

        // --- User typed in too large of a number, ignore it

      }

      // --- New value was selected or typed in

      if (newChoice != -1)

      {

        this.currentIndexComboChoice = newChoice;

        ShowMessage(Resources.MyIndexCombo,

          this.currentIndexComboChoice.ToString(CultureInfo.CurrentCulture));

      }

    }

  }

}

Please note, this is not the full source code of the method, I have omitted a few checks and exception handling statements. IndexCombo expects an integer zero-based index in a string form (InValue). Since our command can be used from the command window, the user might type a string value instead of an index. As a fallback procedure, the method above first tries to parse the InValue string as a valid combo box index. If this validation fails, it tries to find InValue among the strings in the combo box before declaring the value invalid.

MRUCombo uses the same pattern but it only checks if there is any string typed into combo box. Its implementation is quite straightforward, so I do not treat it here. The DynamicCombo implementation is more complex: not only the values (percentage values with a meaning of zoom factor) in the list are accepted, but the user can type any other percentage values, event the “ZoomToFit” and “Zoom to Fit” values are accepted. Here is the implementation of the command handler method:

private double[] numericZoomLevels = { 4.0, 3.0, 2.0, 1.5, 1.25, 1.0, .75, .66,

  .50, .33, .25, .10 };

private string zoomToFit = Resources.ZoomToFit;

private string zoom_to_Fit = Resources.Zoom_to_Fit;

private string[] zoomLevels = null;

private NumberFormatInfo numberFormatInfo;

private double currentZoomFactor = 1.0;

 

private void OnMenuMyDynamicCombo(object sender, EventArgs e)

{

  // --- Some checks omitted

  OleMenuCmdEventArgs eventArgs = e as OleMenuCmdEventArgs;

  if (eventArgs != null)

  {

    object input = eventArgs.InValue;

    IntPtr vOut = eventArgs.OutValue;

    if (vOut != IntPtr.Zero && input != null)

    {

      throw (new ArgumentException(Resources.BothInOutParamsIllegal));

    }

    else if (vOut != IntPtr.Zero)

    {

      // --- The IDE requests for the current value

      if (this.currentZoomFactor == 0)

      {

        Marshal.GetNativeVariantForObject(this.zoom_to_Fit, vOut);

      }

      else

      {

        string factorString = currentZoomFactor.ToString("P0",

          this.numberFormatInfo);

        Marshal.GetNativeVariantForObject(factorString, vOut);

      }

    }

    else if (input != null)

    {

      // --- New zoom value was selected or typed in

      string inputString = input.ToString();

      if (inputString.Equals(this.zoomToFit) ||

        inputString.Equals(this.zoom_to_Fit))

      {

        currentZoomFactor = 0;

        ShowMessage(Resources.MyDynamicCombo, this.zoom_to_Fit);

      }

      else

      {

        try

        {

          float newZoom = Single.Parse(inputString.Replace(

            NumberFormatInfo.InvariantInfo.PercentSymbol, ""),

            CultureInfo.CurrentCulture);

          newZoom = (float)Math.Round(newZoom);

          if (newZoom < 0)

          {

            throw (new ArgumentException(Resources.ZoomMustBeGTZero));             

          }

          currentZoomFactor = newZoom / (float)100.0;

          ShowMessage(Resources.MyDynamicCombo,

            newZoom.ToString(CultureInfo.CurrentCulture));

        }

        catch (FormatException)

        {

          // --- User typed in a non-numeric value, ignore it

        }

        catch (OverflowException)

        {

          // --- user typed in too large of a number, ignore it

        }

      }

    }

  }

}

Please note, a few checks have been omitted from the source code. The method is a bit more complicated than it really should be, but demonstrates that we might have some extra work with dynamic combos:

—  Beside fixed percentage values we accept the “Zoom to Fit” values.

—  The values types by the user are stored as double numbers (indicating the zoom factor); they are displayed as percentage values on the screen.

—  The zoom factor representing the “Zoom to Fit” value is converted back to the appropriate string.

Setting up the dropdown lists

Each combo box except the MRUCombo has to manage the list of dropdown items by its own. This is done through a second command often referred as the “GetList” command. In the overridden Initialize method of the package we bind these commands to the affected combo boxes. All of these command handlers use a simple pattern to retrieve the list of dropdown items. This pattern is illustrated by the source code of the GetList command belonging to the DropDownCombo:

private void OnMenuMyDropDownComboGetList(object sender, EventArgs e)

{

  if ((null == e) || (e == EventArgs.Empty))

  {

    // --- We should never get here; EventArgs are required.

    throw (new ArgumentNullException(Resources.EventArgsRequired));

  }

  OleMenuCmdEventArgs eventArgs = e as OleMenuCmdEventArgs;

  if (eventArgs != null)

  {

    object inParam = eventArgs.InValue;

    IntPtr vOut = eventArgs.OutValue;

    if (inParam != null)

    {

      throw (new ArgumentException(Resources.InParamIllegal));

    }

    else if (vOut != IntPtr.Zero)

    {

      Marshal.GetNativeVariantForObject(this.dropDownComboChoices, vOut);

    }

    else

    {

      throw (new ArgumentException(Resources.OutParamRequired));

    }

  }

}

The EventArgs passed here must be an instance of OleMenuCdmEventArgs—just as in the case of standard combo box commands. The InValue property of the event argument must be null and the OutValue parameter is the location where the list of available items should be stored. The method retrieves back the selection using the dropDownComboChoices member variable which is a string array. We must use the GetNativeVariantForObject method to pass the string array back through the .NET-COM border.

All other GetList commands work on the same way, so here I do not show you their source code.

Improvements in VSXtra

In LearnVSXNow! #24 - Introducing VSXtra I gave you an overview about the VSXtra project that demonstrates how Managed Package Framework could be improved. In this part I show you how easy is to handle combo boxes with VSXtra. I do not tell you all the details, only add a few comments to the source code extracts and let you understand them by your own.

In the C# ComboBox Reference sample all the code is put into the package file. The code functionality is represented there by the overridden Initialize method and by the command handler methods. The VSXtra approach separates the package code from the command handler definitions:

using System.Runtime.InteropServices;

using Microsoft.VisualStudio.Shell;

using VSXtra;

 

namespace DeepDiver.ComboboxCommands

{

  [PackageRegistration(UseManagedResourcesOnly = true)]

  [DefaultRegistryRoot("Software\\Microsoft\\VisualStudio\\9.0")]

  [InstalledProductRegistration(false, "#110", "#112", "1.0", IconResourceID = 400)]

  [ProvideLoadKey("Standard", "1.0", "ComboboxCommands", "DeepDiver", 1)]

  [ProvideMenuResource(1000, 1)]

  [Guid(GuidList.guidComboboxCommandsPkgString)]

  public sealed class ComboboxCommandsPackage : PackageBase

  {

  }

}

With VSXtra, we do not need to override the Initialize method to set up combo box event handlers. The PackageBase class uses the package assembly’s metadata to set up event handlers. We use the same .vsct file as the VS SDK’s reference sample, but use very different event handler code. The following source code declared the event handlers for DropDownCombo, IndexCombo and MRUCombo:

using System.Collections.Generic;

using System.Runtime.InteropServices;

using Microsoft.VisualStudio.Shell;

using VSXtra;

 

namespace DeepDiver.ComboboxCommands

{

  // --- This type represents a command group owned by the ComboboxCommandsPackage.

  [Guid(GuidList.guidComboboxCommandsCmdSetString)]

  public sealed partial class ComboCommandGroup :

    CommandGroup<ComboboxCommandsPackage>

  {

    // --- This command handler responds to the DropDownCombo events.

    [CommandId(CmdIDs.cmdidMyDropDownCombo)]
    [ListCommandId(CmdIDs.cmdidMyDropDownComboGetList)]

    public sealed class DropDownComboCommand : DropDownComboCommandHandler

    {

      protected override IEnumerable<string> GetListValues()

      {

        yield return Resources.Apples;

        yield return Resources.Oranges;

        yield return Resources.Pears;

        yield return Resources.Bananas;

      }

 

      protected override void OnExecute(OleMenuCommand command)

      {

        VsMessageBox.Show(SelectedValue, Resources.MyDropDownCombo);

      }

    }

 

    // --- This command handler responds to the DropDownCombo events.

    [CommandId(CmdIDs.cmdidMyIndexCombo)]
    [ListCommandId(CmdIDs.cmdidMyIndexComboGetList)]

    public sealed class IndexComboCommand : IndexComboCommandHandler

    {

      protected override IEnumerable<string> GetListValues()

      {

        yield return Resources.Lions;

        yield return Resources.Tigers;

        yield return Resources.Bears;

      }

 

      protected override void OnExecute(OleMenuCommand command)

      {

        VsMessageBox.Show(SelectedIndex.ToString(), Resources.MyDropDownCombo);

      }

    }

 

    // --- This command handler responds to the DropDownCombo events.

    [CommandId(CmdIDs.cmdidMyMRUCombo)]

    public sealed class MruComboCommand : MruComboCommandHandler

    {

      protected override void OnExecute(OleMenuCommand command)

      {

        VsMessageBox.Show(SelectedValue, Resources.MyMRUCombo);

      }

    }

  }

}

In this sample ComboCommandGroup represents a logical command group. By deriving this type from CommandGroup<> we can declare that this group is owned by the ComboboxCommandsPackage and all the handlers in this logical group belong to that package.

The three nested class represent command handler types for each combo box. These types derive from the abstract DropDownComboCommandHandler, IndexComboCommandHandler and MruComboCommandHandler abstract classes which implement the basic behavior of the corresponding combo box stereotypes. Using the CommandId and ListCommandId attributes we can associate the handler with the right combo box resources defined in the .vsct file.

You can recognize that the overridden GetListValues methods are iterators using the yield construct introduced in C# 2.0. The OnExecute methods define the action fired when a new item is selected (or typed) in the combo. This implementation totally hides how the current value of the combo box is get or set. We access the combo box values through the SelectedValue property. IndexCombo has a SelectedIndex to access the item index.

The implementation of DynamicCombo uses exactly the same pattern. Its implementation looks like this:

using System;

using System.Collections.Generic;

using System.Globalization;

using Microsoft.VisualStudio.Shell;

using VSXtra;

 

namespace DeepDiver.ComboboxCommands

{

  public partial class ComboCommandGroup

  {

    // --- This command handler responds to the DynamicCombo events.

    [CommandId(CmdIDs.cmdidMyDynamicCombo)]
    [ListCommandId(CmdIDs.cmdidMyDynamicComboGetList)]

    public sealed class DynamicComboCommand : DynamicComboCommandHandler

    {

      private readonly double[] _NumericZoomLevels =

        { 4.0, 3.0, 2.0, 1.5, 1.25, 1.0, .75, .66, .50, .33, .25, .10 };

      private readonly string _ZoomToFit = Resources.ZoomToFit;

      private readonly string _Zoom_to_Fit = Resources.Zoom_to_Fit;

      private readonly NumberFormatInfo _NumberFormatInfo;

 

      // --- Sets up the initial values of the combo box

      public DynamicComboCommand()

      {

        CurrentZoomFactor = 1.0;

        SelectedValue = "100 %";

        _NumberFormatInfo =

          (NumberFormatInfo)CultureInfo.CurrentUICulture.NumberFormat.Clone();

      }

 

      // --- Displays a message box with the selected value.

      protected override void OnExecute(OleMenuCommand command)

      {

        VsMessageBox.Show(CurrentZoomFactor.ToString(), Resources.MyDynamicCombo);

      }

 

      // --- Gets the list used in the DynamicCombo.

      protected override IEnumerable<string> GetListValues()

      {

        for (int i = 0; i < _NumericZoomLevels.Length; i++)

          yield return _NumericZoomLevels[i].ToString("P0", _NumberFormatInfo);

        yield return _Zoom_to_Fit;

      }

 

      // --- Checks if the selected item is valid or not.

      protected override bool IsInputValid(string input, out string output)

      {

        if (input.Equals(_ZoomToFit) || input.Equals(_Zoom_to_Fit))

        {

          CurrentZoomFactor = 0;

          output = _Zoom_to_Fit;

          return true;

        }

        output = input;

        CurrentZoomFactor = 0;

        try

        {

          float newZoom = Single.Parse(input.Replace(

             NumberFormatInfo.InvariantInfo.PercentSymbol, ""),

             CultureInfo.CurrentCulture);

          newZoom = (float)Math.Round(newZoom);

          if (newZoom < 0)

            throw (new ArgumentException(Resources.ZoomMustBeGTZero));

          CurrentZoomFactor = newZoom / (float)100.0;

          output = CurrentZoomFactor.ToString("P0", _NumberFormatInfo);

        }

        catch (FormatException)

        {

          // --- user typed in a non-numeric value, ignore it

        }

        catch (OverflowException)

        {

          // --- user typed in too large of a number, ignore it

        }

        return true;

      }

 

      // --- Gets the currently selected zoom factor

      public double CurrentZoomFactor { get; private set; }

    }

  }

}

The DynamicComboCommandHandler implements the stereotype behavior; we derive the command handler class from it. By overriding the IsInputValid method we can check that the value provided by the user is acceptable by the combo box or not. This method not simply checks the input but also provides the string to be displayed for the input. For example, if we type the value “55” into the combo, it is declared valid and the “55 %” is retrieved in the output variable. The class also provides a CurrentZoomFactor property to access the internally stored value of the combo box.

Where we are?

The Visual Studio Shell allows defining combo boxes for the user interface. These elements use the Combo element in the .vsct file; they can have four behavior types. Combo box command handlers are more complicated than button handlers, because they have more tasks. The “standard” command handler is responsible not only for executing the command action but also for getting and setting the combo value. A separate, so-called “GetList” command is used by the shell to fill the list of the combo dynamically.

In this article a showed how the VS SDK’s C# ComboBox Reference Sample handles combo boxes. My new project called VSXtra makes combo box command handler declarations much easier to read and understand.


Posted Jul 14 2008, 02:52 PM by inovak
Filed under:

Comments

User links about "selectedindex" on iLinkShare wrote User links about "selectedindex" on iLinkShare
on Wed, Mar 11 2009 14:03

Pingback from  User links about "selectedindex" on iLinkShare

tweet wrote re: LearnVSXNow! #25 - Advanced VSCT Concepts: Behind Combos
on Mon, Feb 11 2013 9:10

Thanks for the blog post.Thanks Again. Will read on...

http://www.springbridge.co.uk/categories/Topsoil-/ wrote re: LearnVSXNow! #25 - Advanced VSCT Concepts: Behind Combos
on Sat, Feb 16 2013 12:55

Great, thanks for sharing this blog article.Really thank you! Awesome.

Clomiphene 50 mg wrote re: LearnVSXNow! #25 - Advanced VSCT Concepts: Behind Combos
on Thu, Feb 28 2013 11:06

UJgN32 A big thank you for your blog.Really looking forward to read more. Keep writing.

apple iphone 5c charger wrote apple iphone 5c charger
on Fri, Oct 3 2014 19:21

LearnVSXNow! #25 - Advanced VSCT Concepts: Behind Combos - DiveDeeper's blog - Dotneteers.net