【WPF】ContextMenuのショートカットキーの処理をViewModelに書いてしまう


# 動作

![ContextMenu選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281173/424acad1-7135-6465-23ba-da482bfa9bf2.png)

ボタンを右クリックすると、ContextMenuが、KeyGestureのテキストと共に表示される。

- Action A, Action B, Action Cがクリックされたことを検出する
- メニューが開いている時、キーボード「A」「B」「C」が押されたことを検出する
- ESCボタンが押されたら、ContextMenuが閉じる

例えば、キーボードBが押されると

![Screenshot 2022-09-07 151955.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281173/661f7ca8-8fd5-8cc7-5ac2-61b1f334bc15.png)

こんな表示になるようにする。

# プロジェクトファイル

https://github.com/belre/CSharpCodeSamples/tree/main/ContextMenuKeyHandle/ContextMenuKeyHandle

# 実装

## 準備

動作環境についてはこの記事と同様とする
(.NET Framework 4.8.1＋Visual Studio 2022で、Prism.Wpfを使用)

https://qiita.com/belre/items/0619af6795930a4fe5c1

## 悩みどころ

通常、Controlにキーイベントを割り当てるときは、**KeyBinding**を使用することが多い

https://qiita.com/koara-local/items/02d214f0b6fbf26866ec

つまり、単にキーでBindingしたければ

```XML:MainWindow.xaml
<Button>
 <Button.InputBindings>
   <KeyBinding Key="A" Command="{Binding CommandKeyHandle}" />
 </Button.InputBindings>
</Button>
```

という風に書けばいいわけだが・・・

ここで、ContextMenuでは、実はItemsSourceが使用できる。
これはViewModel等で定義するContextMenuの中身そのものであるが、

例えばxamlをこう定義し

```XML:MainWindow.xaml
<Button.ContextMenu>
 <Button.InputBindings>
   <!-- ※問題が発生する場所※
    Key Aを押したとき、KeyCommandを実行するようにするが、
    バインド時のパスがClickCommandと同じ場所ではなくなるので
    エラーとなってしまう
   -->
   <KeyBinding Key="A" Command="{Binding KeyCommand}" />
 </Button.InputBindings>
 <ContextMenu ItemsSource="{Binding MenuItems}">
  <ContextMenu.ItemContainerStyle>
   <Style TargetType="MenuItem">
    <!-- ContextMenuに表示するテキストの内容を表す -->
    <Setter Property="Header" Value="{Binding MenuText}" />
    <!-- ContextMenuに表示されるキーの表示 -->
    <Setter Property="InputGestureText" Value="{Binding KeyGesture}" />
    <!-- ContextMenuをクリックしたときに実行されるコマンド -->
    <Setter Property="Command" Value="{Binding ClickCommand}"></Setter>
   </Style>
  </ContextMenu.ItemContainerStyle>
 </ContextMenu>
</Button.ContextMenu>
```

ViewModelをこう書いたとすると、

```C#:MainViewModel.cs
public class MainViewModel : BindableBase
{
  public ObservableCollection<MenuViewModel> MenuItems
  {
   get;
   set;
  }

 public MainViewModel()
 {
   MenuItems = new ObservableCollection<MenuViewModel>();
   MenuItems.Add(new MenuViewModel()
   {
     MenuText = "Action A",
     Key = Key.A,
     KeyCommand = new DelegateCommand(RunActionAKey),
     ClickCommand = new DelegateCommand(RunActionAClick)
   });
   MenuItems.Add(new MenuViewModel()
   {
     MenuText = "Action B",
     Key = Key.B
   }); MenuItems.Add(new MenuViewModel()
   {
     MenuText = "Action C",
     Key = Key.C
   });
 }
}

 private void RunActionAClick()
 {
  // Action Aをクリックしたときの処理を書く
 }

 private void RunActionAKey()
 {
  // ContextMenuが開いて'A'のショートカットキーを押されたときの処理を書く
 }

```

```C#:MenuViewModel.cs
public class MenuViewModel
{
 public string MenuText
 {
  get;
  set;
 }

 public Key Key
 {
  get;
  set;
 }

 public string KeyGesture
 {
  get
  {
   return Key.ToString();
  }
 }

 public ICommand KeyCommand
 {
  get;
  set;
 }

 public ICommand ClickCommand
 {
  get;
  set;
 }
}
```

ContextMenuとしては表示が出来るのだが、
KeyCommandがMainViewModelのパスを参照して実行しようとして
コマンドが見つからないという状況になるので、実行時にエラーとなってしまう。

## 改善

単純に、ContextMenuがキー入力を感知するようにすればいいので、
Interaction.Behaviorsを利用して、ビヘイビアを登録する

```XML:MainWindow.xaml
<Button Content="Launch ContextMenu" HorizontalAlignment="Left" Margin="50,54,0,0" VerticalAlignment="Top" Height="25" Width="155">
 <Button.ContextMenu>
  <ContextMenu ItemsSource="{Binding MenuItems}">
   <bh:Interaction.Behaviors>
    <!-- CommandKeyHandleを実行するようにする(依存プロパティが必要) -->
    <user_bh:ContextMenuKeyHandleBehavior CommandKeyHandle="{Binding CommandKeyHandle}" />
   </bh:Interaction.Behaviors>
   <ContextMenu.ItemContainerStyle>
    <Style TargetType="MenuItem">
     <Setter Property="Header" Value="{Binding MenuText}" />
     <Setter Property="InputGestureText" Value="{Binding KeyGesture}"></Setter>
     <Setter Property="Command" Value="{Binding ClickCommand}"></Setter>
    </Style>
   </ContextMenu.ItemContainerStyle>
  </ContextMenu>
 </Button.ContextMenu>
</Button>
```

ここで、新たなビヘイビアとしてContextMenuKeyHandleBehavior.csを追加

```C#:ContextMenuKeyHandleBehavior.cs

public class ContextMenuKeyHandleBehavior : Behavior<ContextMenu>
{
 /* 依存プロパティの定義 */
 public static readonly DependencyProperty CommandKeyHandleProperty = 
  DependencyProperty.Register(nameof(CommandKeyHandle),
   typeof(ICommand),
   typeof(ContextMenuKeyHandleBehavior),
   new PropertyMetadata(null));
 public ICommand CommandKeyHandle
 {
  get
  {
   return (ICommand)GetValue(CommandKeyHandleProperty);
  }
  set
  {
   SetValue(CommandKeyHandleProperty, value);
  }
 }

 /* ビヘイビア生成時の挙動 */
 protected override void OnAttached()
 {
  base.OnAttached();

  // キー入力があった場合に、ContextMenuの処理判定を行う
  AssociatedObject.KeyDown += AssociatedObjectOnKeyDown;
 }

 /* ビヘイビア削除時の挙動 */
 protected override void OnDetaching()
 {
  base.OnDetaching();

  AssociatedObject.KeyDown -= AssociatedObjectOnKeyDown;
 }

 /* キーイベント入力 */
 private void AssociatedObjectOnKeyDown(object sender, KeyEventArgs e)
 {
  // 例として、Escapeキーを押すと、ContextMenuが終了するようにする
  if (e.Key == Key.Escape)
  {
   AssociatedObject.IsOpen = false;
   return;
  }

  // もし所定のキーが入力されたとして、
  // コマンドが定義されていて実行可能であれば、
  // 実行し、ContextMenuが終了するようにする
  if (CommandKeyHandle != null && CommandKeyHandle.CanExecute(e))
  {
   CommandKeyHandle.Execute(e);

   AssociatedObject.IsOpen = false;
  }
 }
}


```

ユーザプロパティの追加方法については次などを参照

https://resanaplaza.com/%E3%80%90wpf%E3%80%91%E4%BE%9D%E5%AD%98%E9%96%A2%E4%BF%82%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%A7%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC/

https://garafu.blogspot.com/2016/01/wpf-dependencyproperty.html

つまり、ビヘイビアに、依存プロパティとしてコマンドを入れておいて、
キー入力があった時に実行するという風にしているだけである。

ViewModelには以下を付与

```C#:MainViewModel.cs
private ICommand _command_key_handle;
public ICommand CommandKeyHandle
{
  get
  {
    return _command_key_handle ?? (_command_key_handle = new DelegateCommand<KeyEventArgs>(HandleKeys, AcceptHandleKeys));
  }
}

private bool AcceptHandleKeys(KeyEventArgs arg)
{
  foreach (var item in MenuItems)
  {
    if (item.Key == arg.Key)
    {
      return true;
    }
  }

  return false;
}
```

# まとめ

- InputGestureTextはショートカットキーの情報を表示するだけ。キー入力のためには、XAMLファイルでKeyBindingを使えばいいが、XAMLファイルで決められたものしか使用することが出来ない
- もし、ContextMenuの内容が変わる可能性があるのであれば、ViewModelにその内容を記述してしまい、その挙動を制御する方が使い勝手が良い