方法: ContextMenuOpening イベントを処理する

アプリケーションで ContextMenuOpening イベントを処理して、既存のコンテキスト メニューを表示する前に調整する、またはイベント データで Handled プロパティを true に設定して、表示されるはずのメニューを非表示にすることができます。 イベント データで Handledtrue に設定する一般的な理由は、メニューを新しい 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 が何であれ、表示するには遅すぎます。 また、ユーザーが 2 回目に右クリックすると、今度は新しい ContextMenu が表示され、値が null でないため、ハンドラーが 2 回目に実行されるときに、ハンドラーにより、メニューが適切に置換され、表示されます。 このことは、次の 2 つの可能な回避策を示唆しています。

  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 であるかチェックすることも、またはコードでフラグを使用して、ハンドラーが少なくとも 1 回実行されているかどうかをチェックすることもできます。 ContextMenu が表示されることを仮定しているため、ハンドラーで、イベント データの Handledtrue に設定します。 コンテキスト メニューの表示を担う ContextMenuService にとって、イベント データ内の Handled の値が true であることは、イベントが発生したコンテキスト メニューとコントロールの組み合わせの表示をキャンセルする要求を表します。

これで問題がある可能性のあるコンテキスト メニューを非表示にしました。次の手順では、新しいものを指定して表示します。 新しいものの設定は、基本的には前のハンドラーと同じです。新しい ContextMenu を作成し、それを使用してコントロール ソースの FrameworkElement.ContextMenu プロパティを設定します。 最初の試行を非表示にしたため、ここでは追加の手順として、コンテキスト メニューを強制的に表示する必要があります。 強制的に表示するには、ハンドラー内で Popup.IsOpen プロパティを true に設定します。 これを行うときは、ハンドラー内でコンテキスト メニューを開くと、ContextMenuOpening イベントが再度発生すことにご注意ください。 再度ハンドラーに入ると、無限再帰になります。 このため、ContextMenuOpening イベント ハンドラー内からコンテキスト メニューを開く場合は常に、null であるかチェックするか、フラグを使用する必要があります。

既存のコンテキスト メニューを非表示にし、コンテキスト メニューを表示しない

最後のシナリオでは、メニューを完全に非表示にするハンドラーを記述しますが、これは一般的ではありません。 特定のコントロールでコンテキスト メニューが表示されないことになっている場合はおそらく、ユーザーが要求したときにのみメニューを非表示にするよりもこれを確実にする適切な方法があるでしょう。 しかし、ハンドラーを使用してコンテキスト メニューを非表示にし、何も表示しないようにしたいのであれば、ハンドラーで、イベント データの Handledtrue に設定する必要があるだけです。 コンテキスト メニューの表示を担う 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;
        }
    }
}

関連項目