Практическое руководство. Событие 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, не имеет уже существующего контекстного меню. Когда пользователь щелкает элемент управления правой кнопкой мыши, ContextMenuOpening вызывается, даже если существующий ContextMenu является пустым или нулевым. Однако в этом случае все новые 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 нулевого значения, либо использовать флаг в коде, чтобы проверить, был ли обработчик запущен хотя бы один раз. Так как предполагается, что ContextMenu отобразится в ближайшее время, обработчик затем задает для Handled значение true в данных события. Для ContextMenuService, который отвечает за отображение контекстного меню, значение true для Handled в данных события представляет запрос на отмену отображения комбинации контекстного меню и элемента управления, которая вызвала событие.

После запрета потенциально подозрительного контекстного меню следующим шагом является предоставление нового меню, а затем его отображение. Настройка нового меню в основном выполняется так же, как и для предыдущего обработчика: вы создаете новое ContextMenu и задаете для него свойство FrameworkElement.ContextMenu источника элемента управления. Дополнительно необходимо принудительно отобразить контекстное меню, так как вы запретили первую попытку отображения. Чтобы принудительно отобразить меню, задайте для свойства Popup.IsOpen значение true в обработчике. Выполняйте это действие с осторожностью, так как открытие контекстного меню в обработчике повторно вызывает событие ContextMenuOpening. Если вы повторно войдете в обработчик, он переходит в бесконечный цикл рекурсии. Поэтому всегда нужно проверять наличие null или использовать флаг, если контекстное меню открывается из обработчика событий ContextMenuOpening.

Запрет отображения всех существующих контекстных меню.

В последнем сценарии редко создается обработчик, который полностью запрещает отображение меню. Если заданный элемент управления не должен отображать контекстное меню, вероятно, есть более подходящие способы обеспечить его отображение, чем запрет отображения меню только по запросу пользователем. Однако если вы хотите использовать обработчик для запрета контекстного меню и ничего не отображать, обработчику нужно просто задать для 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;
        }
    }
}

См. также