如何:处理 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,且处理程序将在处理程序第二次运行时适时替换并显示菜单。 这表明有两种可能的解决方法:

  1. 确保 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>
    
  2. 根据一些初步逻辑,假设初始 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;
        }
    }
}

另请参阅