如何:处理 ContextMenuOpening 事件
可以在应用程序中处理 ContextMenuOpening 事件,以在显示之前调整现有的上下文菜单,或者通过在事件数据中将 Handled 属性设置为 true
来禁止以其他方式显示的菜单。 在事件数据中将 Handled 设置为 true
的原因通常是为了用新的 ContextMenu 对象完全替换菜单,因此有时需要取消操作并启动新的打开。 如果针对 ContextMenuOpening 事件编写处理程序,则应该注意 ContextMenu 控件与通常负责打开和定位控件上下文菜单的服务之间的计时问题。 本主题说明了各种上下文菜单打开方案的一些代码技术,并说明了会出现计时问题的情况。
有多种方案可用于处理 ContextMenuOpening 事件:
在显示前调整菜单项。
在显示前替换整个菜单。
完全取消显示任何现有上下文菜单,且不再显示任何上下文菜单。
示例
在显示前调整菜单项
调整现有菜单项相当简单,而且可能也是最常见的方案。 这样做可能是为了添加或减少上下文菜单选项,以响应应用程序中的当前状态信息或响应特定状态信息(可作为请求上下文菜单的对象上的属性)。
一般做法是获取事件源(即右键单击的特定控件),并从中获取 ContextMenu 属性。 通常需要检查 Items 集合以查看菜单中已存在哪些上下文菜单项,然后在集合中添加或删除适当的新 MenuItem 项。
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);
}
在显示前替换整个菜单
另一种方案则是替换整个上下文菜单。 当然,也可以调整上述代码,删除现有上下文菜单的每个项,然后从 0 开始添加新项。 但是,要替换上下文菜单中的所有项,一个更直观的方法是创建新的 ContextMenu,用项对其进行填充,然后将控件的 FrameworkElement.ContextMenu 属性设置为新的 ContextMenu。
以下是替换 ContextMenu 的简单处理程序代码。 代码引用了自定义 BuildMenu
方法,将该方法分离出来是因为有多个示例处理程序对其进行调用。
void HandlerForCMO(object sender, ContextMenuEventArgs e)
{
FrameworkElement fe = e.Source as FrameworkElement;
fe.ContextMenu = BuildMenu();
}
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;
}
但是,如果对 ContextMenuOpening 使用此类型处理程序,那么当设置 ContextMenu 的对象没有预先具备上下文菜单时,可能会出现计时问题。 用户右键单击控件时,即使现有的 ContextMenu 为空或 NULL,也会引发 ContextMenuOpening。 但在这种情况下,在源元素上设置的任何新的 ContextMenu 都来不及显示。 此外,如果用户碰巧第二次右键单击,那么这一次将显示新的 ContextMenu,该值不为 NULL,且处理程序将在处理程序第二次运行时适时替换并显示菜单。 这表明有两种可能的解决方法:
确保 ContextMenuOpening 处理程序始终针对至少有一个占位符 ContextMenu 可用的控件运行,你计划将该占位符替换为处理程序代码。 在这种情况下,仍然可以使用前面示例中显示的处理程序,但通常需要在初始标记中分配一个占位符 ContextMenu:
<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>
根据一些初步逻辑,假设初始 ContextMenu 值可能为 NULL。 可以检查 ContextMenu 是否为 NULL,或者在代码中使用标志来检查处理程序是否已至少运行一次。 由于假设要显示 ContextMenu,因此处理程序随后会在事件数据中将 Handled 设置为
true
。 对于负责显示上下文菜单的 ContextMenuService,事件数据中的 Handled 值为true
,表示请求取消显示引发事件的上下文菜单/控件组合。
现在,你已取消显示可能出现的可疑上下文菜单,下一步是提供一个新的菜单,然后对其进行显示。 设置新的上下文菜单与之前的处理程序基本相同:需要构建一个新的 ContextMenu,然后使用它设置控件源的 FrameworkElement.ContextMenu 属性。 额外一步是,现在必须强制显示上下文菜单,因为你取消了第一次尝试。 要强制显示,请在处理程序中将 Popup.IsOpen 属性设置为 true
。 执行此操作时要小心,因为在处理程序中打开上下文菜单会再次引发 ContextMenuOpening 事件。 如果重新进入处理程序,则处理程序会无限递归。 因此,在从 ContextMenuOpening 事件处理程序中打开上下文菜单时,始终需要检查是否为 null
或使用标志。
取消显示任何现有上下文菜单并不再显示上下文菜单
最后一种方案(即编写一个完全取消显示菜单的处理程序)并不常见。 如果给定的控件不应该显示上下文菜单,则比起仅在用户请求时取消显示菜单,可能有更合适的方法来确保不显示。 但是,如果想使用处理程序来取消显示上下文菜单并且不显示任何内容,那么处理程序只需在事件数据中将 Handled 设置为 true
。 负责显示上下文菜单的 ContextMenuService 将检查其在控件上引发的事件的事件数据。 如果事件在路由中的任意位置标记为 Handled,则启动事件的上下文菜单打开操作将被取消。
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;
}
}
}