Building a context menu with dynamic command items


Summary
A context menu with dynamic contents should be implemented with a MultiItem.


Defining menus

When you extend ArcGIS for Desktop applications, you might need to define menus or context menus to show with your custom objects. Menus are defined by a class that implements the IMenuDef interface and registers it to the various Esri command bars component categories. Usually, context menus include commands that are shared by all your custom objects and a few extra commands that are tailored for a particular object type.
An ArcMap example is the Topology Error Fix context menu with different fix commands that show depending on the topology error type you have highlighted.
For example, you create two types of custom graphics elements (A and B) that have common functionality (such as Delete and Properties and so on). You also want to add a custom conversion action for the elements, for example, Convert A to B and Convert B to A. Therefore, your context menus will look like the following table:
Element A
Delete
---
Convert A to B
---
Properties…
Element B
Delete
---
Convert B to A
---
Properties…

Logic in client code using the menu

  1. A straightforward implementation is to have two separate context menus. The client code is responsible for determining the menu that shows. See the following code example:
[C#]
public override bool OnContextMenu(int X, int Y)
{
    if (SelectionA)
    {
        //Find ICommandBar using UID of context menu A.
        ContextMenu = ICommandBars.Find(uidCtxA, false, false);
    }
    else if (SelectionB)
    {
        //Find ICommandBar using UID of context menu B.
        ContextMenu = ICommandBars.Find(uidCtxB, false, false);
    }
    ContextMenu.Popup(0, 0);
    return true;
}
[VB.NET]
Public Overloads Overrides Function OnContextMenu(ByVal X As Integer, ByVal Y As Integer) As Boolean
If SelectionA Then
    'Find ICommandBar using UID of context menu A.
    ContextMenu = ICommandBars.Find(uidCtxA, False, False)
ElseIf SelectionB Then
    'Find ICommandBar using UID of context menu B.
    ContextMenu = ICommandBars.Find(uidCtxB, False, False)
End If
ContextMenu.Popup(0, 0)
Return True
End Function
  1. Another implementation is to define a single context menu for both elements and place the appropriate custom commands into the menu programmatically before it shows. See the following code example:
[C#]
public override bool OnContextMenu(int X, int Y)
{
    ContextMenu = ICommandBars.Find(uidCtxShared, false, false);
    ContextMenu.Reset();
    if (SelectionA)
    {
        //Insert command programmatically to:
        ContextMenu.Add(uidCmdA, insertIdx);
    }
    else if (SelectionB)
    {
        ContextMenu.Add(uidCmdB, insertIdx);
    }
    ContextMenu.Popup(0, 0);
    return true;
}
[VB.NET]
Public Overloads Overrides Function OnContextMenu(ByVal X As Integer, ByVal Y As Integer) As Boolean
ContextMenu = ICommandBars.Find(uidCtxShared, False, False)
ContextMenu.Reset()

If SelectionA Then
    'Insert command programmatically to:
    ContextMenu.Add(uidCmdA, insertIdx)
ElseIf SelectionB Then
    ContextMenu.Add(uidCmdB, insertIdx)
End If

ContextMenu.Popup(0, 0)
Return True
End Function
  1. Instead of using a predefined context menu, create one programmatically by calling ICommandBars.Create and insert the items.  

Logic in menu definition

The approaches previously described rely on the client code to make the decision on the command items that should be available. However, you might want to shift this logic to your menu definition. It is tempting to implement your menu shown by the following pseudo code example:
[C#]
public void GetItemInfo(int pos, IItemDef itemDef)
{
    //Since IMenuDef interface doesn't provide a hook object,
    //use the AppRef class to get a ref of the running app.
    IApplication app = new AppRefClass();
    if (pos == 0)
    {
        //Place standard item "Delete."
        itemDef.Value = "{GUID-DELETE-CMD}";
    }
    else if (pos == 1)
    {
        if (SelectionA(app))
        {
            // Place "Convert A to B."
        }
        else if (SelectionB(app))
        {
            // Place "Convert B to A."
        }
    }
    else
    {
        //Place standard item "Properties..."
        itemDef.Value = "{GUID-PROPERTIES-CMD}";
    }
}
[VB.NET]
Public Sub GetItemInfo(ByVal pos As Integer, ByVal itemDef As IItemDef)
    'Since IMenuDef interface doesn't provide a hook object,
    'use the AppRef class to get a ref of the running app.
    Dim app As IApplication = New AppRefClass()
    
    If pos = 0 Then
        'Place standard item "Delete."
        itemDef.Value = "{GUID-DELETE-CMD}"
    ElseIf pos = 1 Then
        If SelectionA(app) Then
            ' Place "Convert A to B."
        ElseIf SelectionB(app) Then
            ' Place "Convert B to A."
        End If
    Else
        'Place standard item "Properties..."
        itemDef.Value = "{GUID-PROPERTIES-CMD}"
    End If
End Sub
The client code that pops up the context menu is simplified as shown in the following code example:
[C#]
public override bool OnContextMenu(int X, int Y)
{
    ContextMenu = ICommandBars.Find(uidCtxShared, false, false);
    ContextMenu.Popup();
}
[VB.NET]
Public Overloads Overrides Function OnContextMenu(ByVal X As Integer, ByVal Y As Integer) As Boolean
ContextMenu = ICommandBars.Find(uidCtxShared, False, False)
ContextMenu.Popup()
End Function
This worked prior to ArcGIS 9.2 because usually a new instance of the context menu was created and the IMenuDef construction code was invoked when ICommandBars.Find was called. At ArcGIS 9.2 and later, this is no longer valid because the context menu is following the same singleton pattern as a toolbar. This means when a context menu has been created once in a document session, the same context menu instance will be returned the next time you request it during the same document session. Therefore, you can no longer rely on the code in IMenuDef.GetItemInfo to update your context menu.
In the previous example, suppose the first time you requested the context menu with its user interface design (UID) and IMenuDef.GetItemInfo added a command for element A to the menu. See the following:
Delete
---
Convert A to B
---
Properties…
When the menu is requested again in the same document session, it still has command A, and element B is included as well.
If you have written menus this way, you need to change this method. If only a few dynamic context menus are needed, you can use the separate context menu implementation described in Step 1. However, if you don't want to rely on client code to place the menu items or create separate classes, implement a MultiItem command instead.
IMultiItem.OnPopUp is called when the item shows, so you can verify the appropriate command will be placed on the menu. Another benefit of a MultiItem implementation is that you can get the application directly from the incoming hook parameter of IMultiItem.OnPopUp instead of the AppRefClass in an IMenuDef implementation.
For hints on diagnosing menus, see Checking menu implementation in this topic.

Using a MultiItem to place commands

A recommended implementation is to use a MultiItem to place dynamic contents on a menu. See the following code example:
[C#]
//
// MultiItem command class.
//
List m_cmds; //Array of commands.

public int OnPopup(object Hook)
{
    IApplication app = (IApplication)hook;
    m_cmds = new List();

    if (SelectionA(app))
    {
        ICommand cmd = ICommandBars.Find(uidCmdA, false, false).Command;
        m_cmds.Add(cmd);
    }
    else if (SelectionB(app))
    {
        ICommand cmd = ICommandBars.Find(uidCmdB, false, false).Command;
        m_cmds.Add(cmd);

    }
}

public string get_ItemCaption(int index)
{
    ICommand cmd = m_cmds[idx];
    return cmd.Caption;
}

public void OnItemClick(int index)
{
    ICommand cmd = m_cmds[idx];
    cmd.OnClick();
}

... 

//
// Menu definition class.
//
public void GetItemInfo(int pos, IItemDef itemDef)
{
    if (pos == 0)
    {
        ...
    }
    else if (pos == 1)
    {
        itemdef.Value = "{GUID-MULTI-ITEM}";
    }
    else
    {
        ...
    }
}
[VB.NET]
'
' MultiItem command class.
'
Private m_cmds As List

'Array of commands.

Public Function OnPopup(ByVal Hook__1 As Object) As Integer
    Dim app As IApplication = DirectCast(hook, IApplication)
    m_cmds = New List()
    
    If SelectionA(app) Then
        Dim cmd As ICommand = ICommandBars.Find(uidCmdA, False, False).Command
        m_cmds.Add(cmd)
    ElseIf SelectionB(app) Then
        Dim cmd As ICommand = ICommandBars.Find(uidCmdB, False, False).Command
        
        m_cmds.Add(cmd)
    End If
End Function


Public Function get_ItemCaption(ByVal index As Integer) As String
    Dim cmd As ICommand = m_cmds(idx)
    Return cmd.Caption
End Function


Public Sub OnItemClick(ByVal index As Integer)
    Dim cmd As ICommand = m_cmds(idx)
    cmd.OnClick()
End Sub

...

'
' Menu definition class.
'

Public Sub GetItemInfo(ByVal pos As Integer, ByVal itemDef__1 As IItemDef)
    If pos = 0 Then
        ...
    ElseIf pos = 1 Then
        itemdef.Value = "{GUID-MULTI-ITEM}"
    Else
        ...
    End If
End Sub
For more information on MultiItem implementation, see How to create dynamic menu commands using a MultiItem.

Checking menu implementation

Verify dynamic contents are implemented in your menu definition class—especially in IMenuDef.GetItemInfo. See the following:
  • If you need to get a reference to the running application, its extension, document, and so on—for example, new AppRef—it is likely you have a dynamic menu.
  • Check the logic you use to read component categories to insert items in the menu.
  • Check the switch/case or if/elseif/else statements.
Consider updating your dynamic menu implementation using MultiItem as previously outlined.


See Also:

Creating toolbars and menus
How to create dynamic menu commands using a MultiItem