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:
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>
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;
}
}
}