Dela via


How to: Handle the ContextMenuOpening Event

The ContextMenuOpening event can be handled in an application to either adjust an existing context menu prior to display or to suppress the menu that would otherwise be displayed by setting the Handled property to true in the event data. The typical reason for setting Handled to true in the event data is to replace the menu entirely with a new ContextMenu object, which sometimes requires canceling the operation and starting a new open. If you write handlers for the ContextMenuOpening event, you should be aware of timing issues between a ContextMenu control and the service that is responsible for opening and positioning context menus for controls in general. This topic illustrates some of the code techniques for various context menu opening scenarios and illustrates a case where the timing issue comes into play.

There are several scenarios for handling the ContextMenuOpening event:

  • Adjusting the menu items before display.

  • Replacing the entire menu before display.

  • Completely suppressing any existing context menu and displaying no context menu.

Example

Adjusting the Menu Items Before Display

Adjusting the existing menu items is fairly simple and is probably the most common scenario. You might do this in order to add or subtract context menu options in response to current state information in your application or particular state information that is available as a property on the object where the context menu is requested.

The general technique is to get the source of the event, which is the specific control that was right-clicked, and get the ContextMenu property from it. You typically want to check the Items collection to see what context menu items already exist in the menu, and then add or remove appropriate new MenuItem items to or from the collection.

        Private Sub AddItemToCM(ByVal sender As Object, ByVal e As ContextMenuEventArgs)
            'check if Item4 is already there, this will probably run more than once
            Dim fe As FrameworkElement = TryCast(e.Source, FrameworkElement)
            Dim cm As ContextMenu = fe.ContextMenu
            For Each mi As MenuItem In cm.Items
                If CType(mi.Header, String) = "Item4" Then
                    Return
                End If
            Next mi
            Dim mi4 As New MenuItem()
            mi4.Header = "Item4"
            fe.ContextMenu.Items.Add(mi4)
        End Sub
void AddItemToCM(object sender, ContextMenuEventArgs e)
{
    //check if Item4 is already there, this will probably run more than once
    FrameworkElement fe = e.Source as FrameworkElement;
    ContextMenu cm = fe.ContextMenu;
    foreach (MenuItem mi in cm.Items)
    {
        if ((String)mi.Header == "Item4") return;
    }
    MenuItem mi4 = new MenuItem();
    mi4.Header = "Item4";
    fe.ContextMenu.Items.Add(mi4);
}

Replacing the Entire Menu Before Display

An alternative scenario is if you want to replace the entire context menu. You could of course also use a variation of the preceding code, to remove every item of an existing context menu and add new ones starting with item zero. But the more intuitive approach for replacing all items in the context menu is to create a new ContextMenu, populate it with items, and then set the FrameworkElement.ContextMenu property of a control to be the new ContextMenu.

The following is the simple handler code for replacing a ContextMenu. The code references a custom BuildMenu method, which is separated out because it is called by more than one of the example handlers.

        Private Sub HandlerForCMO(ByVal sender As Object, ByVal e As ContextMenuEventArgs)
            Dim fe As FrameworkElement = TryCast(e.Source, FrameworkElement)
            fe.ContextMenu = BuildMenu()
        End Sub
void HandlerForCMO(object sender, ContextMenuEventArgs e)
{
    FrameworkElement fe = e.Source as FrameworkElement;
    fe.ContextMenu = BuildMenu();
}
        Private Function BuildMenu() As ContextMenu
            Dim theMenu As New ContextMenu()
            Dim mia As New MenuItem()
            mia.Header = "Item1"
            Dim mib As New MenuItem()
            mib.Header = "Item2"
            Dim mic As New MenuItem()
            mic.Header = "Item3"
            theMenu.Items.Add(mia)
            theMenu.Items.Add(mib)
            theMenu.Items.Add(mic)
            Return theMenu
        End Function
ContextMenu BuildMenu()
{
    ContextMenu theMenu = new ContextMenu();
    MenuItem mia = new MenuItem();
    mia.Header = "Item1";
    MenuItem mib = new MenuItem();
    mib.Header = "Item2";
    MenuItem mic = new MenuItem();
    mic.Header = "Item3";
    theMenu.Items.Add(mia);
    theMenu.Items.Add(mib);
    theMenu.Items.Add(mic);
    return theMenu;
}

However, if you use this style of handler for ContextMenuOpening, you can potentially expose a timing issue if the object where you are setting the ContextMenu does not have a preexisting context menu. When a user right-clicks a control, ContextMenuOpening is raised even if the existing ContextMenu is empty or null. But in this case, whatever new ContextMenu you set on the source element arrives too late to be displayed. Also, if the user happens to right-click a second time, this time your new ContextMenu appears, the value is non null, and your handler will properly replace and display the menu when the handler runs a second time. This suggests two possible workarounds:

  1. Insure that ContextMenuOpening handlers always run against controls that have at least a placeholder ContextMenu available, which you intend to be replaced by the handler code. In this case, you can still use the handler shown in the previous example, but you typically want to assign a placeholder ContextMenu in the initial markup:

    <StackPanel>
      <Rectangle Fill="Yellow" Width="200" Height="100" ContextMenuOpening="HandlerForCMO">
        <Rectangle.ContextMenu>
          <ContextMenu>
            <MenuItem>Initial menu; this will be replaced ...</MenuItem>
          </ContextMenu>
        </Rectangle.ContextMenu>
      </Rectangle>
      <TextBlock>Right-click the rectangle above, context menu gets replaced</TextBlock>
    </StackPanel>
    
  2. Assume that the initial ContextMenu value might be null, based on some preliminary logic. You could either check ContextMenu for null, or use a flag in your code to check whether your handler has been run at least once. Because you assume that the ContextMenu is about to be displayed, your handler then sets Handled to true in the event data. To the ContextMenuService that is responsible for context menu display, a true value for Handled in the event data represents a request to cancel the display for the context menu / control combination that raised the event.

Now that you have suppressed the potentially suspect context menu, the next step is to supply a new one, then display it. Setting the new one is basically the same as the previous handler: you build a new ContextMenu and set the control source's FrameworkElement.ContextMenu property with it. The additional step is that you must now force the display of the context menu, because you suppressed the first attempt. To force the display, you set the Popup.IsOpen property to true within the handler. Be careful when you do this, because opening the context menu in the handler raises the ContextMenuOpening event again. If you reenter your handler, it becomes infinitely recursive. This is why you always need to check for null or use a flag if you open a context menu from within a ContextMenuOpening event handler.

Suppressing Any Existing Context Menu and Displaying No Context Menu

The final scenario, writing a handler that suppresses a menu totally, is uncommon. If a given control is not supposed to display a context menu, there are probably more appropriate ways to assure this than by suppressing the menu just when a user requests it. But if you want to use the handler to suppress a context menu and show nothing, then your handler should simply set Handled to true in the event data. The ContextMenuService that is responsible for displaying a context menu will check the event data of the event it raised on the control. If the event was marked Handled anywhere along the route, then the context menu open action that initiated the event is suppressed.

        Private Sub HandlerForCMO2(ByVal sender As Object, ByVal e As ContextMenuEventArgs)
            If Not FlagForCustomContextMenu Then
                e.Handled = True 'need to suppress empty menu
                Dim fe As FrameworkElement = TryCast(e.Source, FrameworkElement)
                fe.ContextMenu = BuildMenu()
                FlagForCustomContextMenu = True
                fe.ContextMenu.IsOpen = True
            End If
        End Sub
    End Class
        void HandlerForCMO2(object sender, ContextMenuEventArgs e)
        {
            if (!FlagForCustomContextMenu)
            {
                e.Handled = true; //need to suppress empty menu
                FrameworkElement fe = e.Source as FrameworkElement;
                fe.ContextMenu = BuildMenu();
                FlagForCustomContextMenu = true;
                fe.ContextMenu.IsOpen = true;
            }
        }
    }

See Also

Reference

ContextMenu

FrameworkElement.ContextMenu

Concepts

Base Elements Overview

ContextMenu Overview