WPFでコンテキストメニューの項目をコレクションとバインドし項目を動的に追加する

C# コンピュータ
C#

メインメニューなどはあらかじめ定義された機能を呼び出すので、XAMLで静的に定義しても良いですが、最近使ったファイル(Recent)など動的に作っているメニュー項目がどのように作っているか調べてみました。

ファイル名:MainWindow.xaml

<Window
x:Class="DynamicMenu01.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DynamicMenu01"
mc:Ignorable="d"
Title="タイトル"
Height="450"
Width="800"
xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors">
  <Window.DataContext>
    <local:MainWindowViewModel />
  </Window.DataContext>
  <Grid>
    <TextBox Text="{Binding TextBoxText.Value}">
        <TextBox.ContextMenu>
            <ContextMenu ItemsSource="{Binding ContextMenuItems}">
                <ContextMenu.ItemContainerStyle>
                  <Style TargetType="MenuItem">
                      <Setter Property="Header" Value="{Binding Path=Header}"/>
                      <Setter Property="Command" Value="{Binding Path=MyCommand}"/>
                  </Style>
              </ContextMenu.ItemContainerStyle>                  
            </ContextMenu>
        </TextBox.ContextMenu>
    </TextBox>
  </Grid>
</Window>

ファイル名:MenuItemViewModel.cs

using System.ComponentModel;
using Reactive.Bindings;
public class MenuItemViewModel : INotifyPropertyChanged
{
    public string Header { get; set; } = "";
    public ReactiveCommand<EventArgs> MyCommand { get; set; } = new();
    
    // INotifyPropertyChanged
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged(string name) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    
    
}

ファイル名:MainWindowViewModel.cs

using System.Diagnostics;
using System.ComponentModel;
using Reactive.Bindings;

namespace DynamicMenu01;
public class MainWindowViewModel : INotifyPropertyChanged
{
    // INotifyPropertyChanged
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged(string name) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

    /**************************************************************************
    * プロパティ
    **************************************************************************/
    public ReactiveCollection<MenuItemViewModel> ContextMenuItems { get; set; } = [];
    public ReactiveProperty<string> TextBoxText { get; private set; } = new("");
    public MainWindowViewModel()
    {
        PropertyChanged += (s, e) => {};
        var mi1 = new MenuItemViewModel
        {
            Header = "ファイル",
        };
        mi1.MyCommand.Subscribe(e=>
        {
            TextBoxText.Value="ファイル";
        });

        ContextMenuItems.AddOnScheduler(mi1);
        var mi2 = new MenuItemViewModel
        {
            Header = "編集",
        };
        mi2.MyCommand.Subscribe(e=>
        {
            TextBoxText.Value="編集";
        });
        ContextMenuItems.AddOnScheduler(mi2);
    }
}

テキストボックスで右クリックコンテキストメニューが表示されます。

項目を選択するとテキストボックスに文字がセットされます。

コンテキストメニューの「ファイル」や「編集」などの項目は、MainWindowViewModelのContextMenuItemsコレクションにコンストラクタで動的に追加しています。
ContextMenuItemsはMainWindow.xamlのContextMenuのItemsSourceにバインドしています。
メニューの表示名にあたるHeaderや項目をクリックしたさい呼び出されるCommandはContextMenu.ItemContainerStyle内でバインドしています。
このあたりよく理解できていないのですが、MenuItem.ItemTemplateを使う方法では上手く動作されることが出来ませんでした。メニューの表示まではたどり着けましたが、Commandが反応しませんでした。

コメント