Walkthrough: Dynamically Adding Menu Items

You can add menu items at run time by specifying the DynamicItemStart command flag on a placeholder button definition in the Visual Studio command-table (.vsct) file, then defining (in code) the number of menu items to display and handling the command(s). When the VSPackage is loaded, the placeholder is replaced with the dynamic menu items.

This walkthrough shows how to set the startup project in a Visual Studio solution, using a command on the Solution Explorer toolbar. It uses a menu controller that has a dynamic dropdown list of the projects in the active solution. To keep this command from appearing when no solution is open or when the open solution has only one project, the VSPackage is loaded only when a solution has multiple projects.

For more information about .vsct files, see Visual Studio Command Table (.Vsct) Files.

Creating the VSPackage

Setting up the elements in the .vsct file

To create a menu controller with dynamic menu items on a toolbar, you specify the following elements:

  • Two command groups, one that contains the menu controller and another that contains the menu items in the dropdown

  • One menu element of type MenuController

  • Two buttons, one that acts as the placeholder for the menu items and another that supplies the icon and the tooltip on the toolbar.

  1. Open DynamicMenuItems.vsct.

  2. Define the command IDs. Go to the Symbols section and replace the IDSymbol elements in the guidDynamicMenuItemsCmdSetGuidSymbol with IDSymbol elements for the two groups, the menu controller, the placeholder command, and the anchor command.

    <GuidSymbol name=“guidDynamicMenuItemsCmdSet " value="{ your GUID here }">
        <IDSymbol name="MyToolbarItemGroup" value="0x1020" />
        <IDSymbol name="MyMenuControllerGroup" value="0x1025" />
        <IDSymbol name="MyMenuController" value ="0x1030"/>
        <IDSymbol name="cmdidMyAnchorCommand" value="0x0103" />
        <!-- NOTE: The following command expands at run time to some number of ids.
         Try not to place command ids after it (e.g. 0x0105, 0x0106).
         If you must add a command id after it, make the gap very large (e.g. 0x200) -->
        <IDSymbol name="cmdidMyDynamicStartCommand" value="0x0104" />
    </GuidSymbol>  
    
  3. Add the two groups:

    <Groups>
        <!-- The group that adds the MenuController on the Solution Explorer toolbar. 
             The 0x4000 priority adds this group after the group that contains the
             Preview Selected Items button, which is normally at the far right of the toolbar. -->
        <Group guid="guidDynamicMenuItemsCmdSet" id="MyToolbarItemGroup" priority="0x4000" >
            <Parent guid="guidSHLMainMenu" id="IDM_VS_TOOL_PROJWIN" />
        </Group>
        <!-- The group for the items on the MenuController drop-down. It is added to the MenuController submenu. -->
        <Group guid="guidDynamicMenuItemsCmdSet" id="MyMenuControllerGroup" priority="0x4000" >
            <Parent guid="guidDynamicMenuItemsCmdSet" id="MyMenuController" />
        </Group>
    </Groups>
    

    Add the MenuController. Set the DynamicVisibility command flag, since it is not always visible. The ButtonText is not displayed.

    <Menus>
        <!-- The MenuController to display on the Solution Explorer toolbar.
             Place it in the ToolbarItemGroup.-->
        <Menu guid="guidDynamicMenuItemsCmdSet " id="MyMenuController" priority="0x1000" type="MenuController">
            <Parent guid="guidDynamicMenuItemsCmdSet" id="MyToolbarItemGroup" />
            <CommandFlag>DynamicVisibility</CommandFlag>
            <Strings>
               <ButtonText></ButtonText>
           </Strings>
        </Menu>
    </Menus>
    
  4. Add two buttons, one as a placeholder for the dynamic menu items and one as an anchor for the MenuController.

    The parent of the placeholder button is the MyMenuControllerGroup. Add the DynamicItemStart, DynamicVisibility, and TextChanges command flags to the placeholder button. The ButtonText is not displayed.

    The anchor button holds the icon and the tooltip text. The parent of the anchor button is also the MyMenuControllerGroup. You add the NoShowOnMenuController command flag to make sure the button doesn’t actually appear in the menu controller dropdown, and the FixMenuController command flag to make it the permanent anchor.

    <!-- The placeholder for the dynamic items that expand to N items at runtime. -->
    <Buttons>
        <Button guid="guidDynamicMenuItemsCmdSet" id="cmdidMyDynamicStartCommand" priority="0x1000" >        
            <Parent guid="guidDynamicMenuItemsCmdSet" id="MyMenuControllerGroup" />
            <CommandFlag>DynamicItemStart</CommandFlag>
            <CommandFlag>DynamicVisibility</CommandFlag>
            <CommandFlag>TextChanges</CommandFlag>
            <!-- This text does not appear. -->
            <Strings>
              <ButtonText>Project</ButtonText>
            </Strings>
          </Button>
    
          <!-- The anchor item to supply the icon/tooltip for the MenuController -->
          <Button guid=“guidDynamicMenuItemsCmdSet" id="cmdidMyAnchorCommand" priority="0x0000" >
            <Parent guid=“guidDynamicMenuItemsCmdSet" id="MyMenuControllerGroup" />
            <!-- This is the icon that appears on the Solution Explorer toolbar. -->
            <Icon guid="guidImages" id="bmpPicArrows"/>
            <!-- Do not show on the menu controller's drop down list-->
            <CommandFlag>NoShowOnMenuController</CommandFlag>
            <!-- Become the permanent anchor item for the menu controller -->
            <CommandFlag>FixMenuController</CommandFlag>
            <!-- The text that appears in the tooltip.-->
            <Strings>
              <ButtonText>Set Startup Project</ButtonText>
            </Strings>
        </Button>
    </Buttons>
    
  5. Add an icon to the project (in the Resources folder), and then add the reference to it in the .vsct file. In this walkthrough, we use the Arrows icon that's included in the project template.

    <Bitmaps>
        <Bitmap guid="guidImages" href="Resources\Images.png" usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows"/>
    </Bitmaps>
    
  6. Add a VisibilityConstraints section outside the Commands section just before the Symbols section. (You may get a warning if you add it after Symbols.) This section makes sure that the menu controller appears only when a solution with multiple projects is loaded.

    <VisibilityConstraints>
         <!--Make the MenuController show up only when there is a solution with more than one project loaded-->
        <VisibilityItem guid="guidDynamicMenuItemsCmdSet" id="MyMenuController" context="UICONTEXT_SolutionHasMultipleProjects"/>
    </VisibilityConstraints>
    

Implementing the dynamic menu command

You create a dynamic menu command class that inherits from OleMenuCommand. In this implementation, the constructor specifies a predicate to be used for matching commands. You must override the DynamicItemMatch method to use this predicate to set the MatchedCommandId property, which identifies the command to be invoked.

  1. Add a class named DynamicItemMenuCommand that inherits from OleMenuCommand:

    class DynamicItemMenuCommand : OleMenuCommand
    {
    
    }
    
  2. Add a private field to store the match predicate:

    private Predicate<int> matches;
    
  3. Add a constructor that inherits from the OleMenuCommand constructor and specifies a command handler and a BeforeQueryStatus handler. Add a predicate for matching the command:

    public DynamicItemMenuCommand(CommandID rootId, Predicate<int> matches, EventHandler invokeHandler, EventHandler beforeQueryStatusHandler)
        : base(invokeHandler, null /*changeHandler*/, beforeQueryStatusHandler, rootId)
    {
        if (matches == null)
        {
            throw new ArgumentNullException("matches");
        }
    
        this.matches = matches;
    }
    
  4. Override the DynamicItemMatch method so that it calls the matches predicate and sets the MatchedCommandId property:

    public override bool DynamicItemMatch(int cmdId)
    {
        // Call the supplied predicate to test whether the given cmdId is a match.
        // If it is, store the command id in MatchedCommandid 
        // for use by any BeforeQueryStatus handlers, and then return that it is a match.
        // Otherwise clear any previously stored matched cmdId and return that it is not a match.
        if (this.matches(cmdId))
        {
            this.MatchedCommandId = cmdId;
            return true;
        }
    
        this.MatchedCommandId = 0;
        return false;
    }
    

Adding the command in the Initialize() method of the VSPackage

The Initialize() method of the VSPackage is where you set up menu commands, including dynamic menus and menu items.

  1. Open DynamicMenuItemsPackage.cs and delete the default constructor, the existing Initialize() method, and the MenuItemCallback handler.

  2. Add a private field dte2 to the DynamicMenuItemsPackage class that we will use in setting the startup project. You must also add using declarations for EnvDTE and EnvDTE80.

    using EnvDTE;
    using EnvDTE80;
    . . .
    public sealed class DynamicMenuItemsPackage : Package
    {
        private DTE2 dte2;
    . . .
    }
    
  3. Add an Initialize() method that adds the menu command. In the next section we'll define the command handler, the BeforeQueryStatus event handler, and the match predicate.

    protected override void Initialize()
    {
        base.Initialize();
    
        OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
        if (null != mcs)
        {
        // Add the DynamicItemMenuCommand for the expansion of the root item into N items at run time. 
            CommandID dynamicItemRootId = new CommandID(GuidList.guidDynamicMenuItemsCmdSet, (int)PkgCmdIDList.cmdidMyCommand);
            DynamicItemMenuCommand dynamicMenuCommand 
                new DynamicItemMenuCommand(dynamicItemRootId,
                                           IsValidDynamicItem,
                                           OnInvokedDynamicItem,
                                           OnBeforeQueryStatusDynamicItem);
            mcs.AddCommand(dynamicMenuCommand);
        }
        // Get the DTE2 object to use in enumerating the projects in a solution
        dte2 = (DTE2)GetService(typeof(DTE));
    }
    

Implementing the handlers

To implement dynamic menu items on a menu controller, you must handle the command when a dynamic item is clicked. You must also implement the logic that sets the state of the menu item.

  1. To implement the Set Startup Project command, add the OnInvokedDynamicItem event handler. It looks for the project whose name is the same as the text of the command that has been invoked, and sets it as the startup project by setting its absolute path in the StartupProjects property.

    private void OnInvokedDynamicItem(object sender, EventArgs args)
    {
        DynamicItemMenuCommand invokedCommand = (DynamicItemMenuCommand)sender;
        // If the command is already checked, we don’t need to do anything
        if (invokedCommand.Checked)
            return;
    
        // Find the project that corresponds to the command text and set it as the startup project
        var projects = dte2.Solution.Projects;
        foreach (Project proj in projects)
        {
            if (invokedCommand.Text.Equals(proj.Name))
            {
                dte2.Solution.SolutionBuild.StartupProjects = proj.FullName;
                    return;
            }
        }
    }
    
  2. Add the OnBeforeQueryStatusDynamicItem event handler. This is the handler called before a QueryStatus event. It determines whether the menu item is a “real” item, that is, not the placeholder item, and whether the item is already checked (meaning that the project is already set as the startup project).

    private void OnBeforeQueryStatusDynamicItem(object sender, EventArgs args)
    {
        DynamicItemMenuCommand matchedCommand = (DynamicItemMenuCommand)sender;
        matchedCommand.Enabled = true;
        matchedCommand.Visible = true;
    
        // Find out whether the command ID is 0, which is the ID of the root item.
        // If it is the root item, it matches the constructed DynamicItemMenuCommand,
        // and IsValidDynamicItem won't be called.
        bool isRootItem = (matchedCommand.MatchedCommandId == 0);
    
        // The index is set to 1 rather than 0 because the Solution.Projects collection is 1-based.
        int indexForDisplay = (isRootItem ? 1 : (matchedCommand.MatchedCommandId - (int)PkgCmdIDList.cmdidMyCommand) + 1);
    
        matchedCommand.Text = dte2.Solution.Projects.Item(indexForDisplay).Name;
    
        Array startupProjects = (Array)dte2.Solution.SolutionBuild.StartupProjects;
        string startupProject = System.IO.Path.GetFileNameWithoutExtension((string)startupProjects.GetValue(0));
    
        // Check the command if it isn't checked already selected
        matchedCommand.Checked = (matchedCommand.Text == startupProject);
    
        // Clear the ID because we are done with this item.
        matchedCommand.MatchedCommandId = 0;
    }  
    

Implementing the command ID match predicate

  • Now implement the match predicate. We need to determine two things: first, whether the command ID is valid (it is greater than or equal to the declared command ID), and second, whether it specifies a possible project (it is less than the number of projects in the solution).

    private bool IsValidDynamicItem(int commandId)
    {
        // The match is valid if the command ID is >= the id of our root dynamic start item 
        // and the command ID minus the ID of our root dynamic start item
        // is less than or equal to the number of projects in the solution.
        return (commandId >= (int)PkgCmdIDList.cmdidMyCommand) && ((commandId - (int)PkgCmdIDList.cmdidMyCommand) < dte2.Solution.Projects.Count);
    }
    

Setting the VSPackage to load only when a solution has multiple projects

Because the Set Startup Project command doesn’t make sense unless the active solution has more than one project, you can set your VSPackage to auto-load only in that case. You use ProvideAutoLoadAttribute together with the UI context SolutionHasMultipleProjects. Your VSPackage declaration should look like this:

[PackageRegistration(UseManagedResourcesOnly = true)]
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
[ProvideMenuResource("Menus.ctmenu", 1)]
[ProvideAutoLoad(UIContextGuids.SolutionHasMultipleProjects)]
[Guid(GuidList.guidDynamicMenuItemsPkgString)]
public sealed class DynamicMenuItemsPackage : Package
{}

Testing the Set Startup Project command

Now you can test your code.

  1. Start debugging the project.

  2. In the experimental instance, open a solution that has more than one project.

    You should see the arrow icon on the Solution Explorer toolbar. When you expand it, menu items that represent the different projects in the solution should appear.

  3. When you check one of the projects, it becomes the startup project.

  4. When you close the solution, or open a solution that has only one project, the toolbar icon should disappear.

See Also

Concepts

How VSPackages Add User Interface Elements to the IDE

Other Resources

Commands, Menus, and Toolbars

Walkthroughs for User Interface Elements