如何:处理 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);
}

在显示之前替换整个菜单

另一种情况是,如果要替换整个上下文菜单。 当然,还可以使用上述代码的变体来删除现有上下文菜单的每个项,并添加以项零开头的新项。 但是,替换上下文菜单中所有项的更直观的方法是创建新的 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 值可能为空,这取决于一些初步逻辑。 可以检查 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;
        }
    }
}

另请参阅