WPFでコンテキストメニューの項目を動的に生成する方法を試して見ました。
コンテキストメニューは親コントロールがあるわけでは無いので、DataContextを直接指定する必要があるとのこと。
メニューのヘッダーは表示するのにコマンドが実行されないなど、結構理不尽にも思えるトラブルにも遭遇しましたが、
何とか「ListViewで右クリック時、バインドされたメニューを動的に表示する」までたどり着きました。
動作イメージ
1.起動するとリストビューが表示される。
2.右クリックでコンテキストメニューが表示される。
3.メッセージボックス
ソースコード
ファイル名:ListViewItem.cs
using System.ComponentModel;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;
namespace ListViewOnContextMenu01;
public class ListViewItem: INotifyPropertyChanged, IDisposable
{
#region
// INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
// IDisposable
private CompositeDisposable Disposable { get; } = [];
public void Dispose() => Disposable.Dispose();
#endregion
public string Name { get; set; } = "";
public ReactiveCollection<MenuItemViewModel> ContextMenuItems { get; set; } = [];
// コンストラクタ
public ListViewItem()
{
ContextMenuItems.AddTo(Disposable);
}
}
ファイル名:MainWindow.xaml
<Window x:Class="ListViewOnContextMenu01.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:ListViewOnContextMenu01"
mc:Ignorable="d"
Title="Title"
Height="450"
Width="800"
FontSize="16"
xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<i:Interaction.Behaviors>
<local:ViewModelCleanupBehavior />
</i:Interaction.Behaviors>
<Grid>
<ListView
SelectedItem="{Binding SelectedItem.Value}"
ItemsSource="{Binding Items}">
<!-- リストビュー コンテキストメニュー から -->
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu
DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}"
ItemsSource="{Binding ContextMenuItems}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Header}" />
<Setter Property="Command" Value="{Binding Command}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
<!-- リストビュー コンテキストメニュー ここまで -->
<ListView.View>
<GridView>
<GridViewColumn Width="300" Header="名前">
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Width="Auto" TextAlignment="Left" Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
ファイル名:MainWindowViewModel.cs
using System.Diagnostics;
using System.ComponentModel;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;
using System.Windows;
namespace ListViewOnContextMenu01;
public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
{
#region
// INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
// IDisposable
private CompositeDisposable Disposable { get; } = [];
public void Dispose() => Disposable.Dispose();
#endregion
/**************************************************************************
* プロパティ
**************************************************************************/
public ReactiveCollection<ListViewItem> Items { get; set; } = [];
public ReactiveProperty<ListViewItem> SelectedItem { get; set; } = new();
public MainWindowViewModel()
{
Items.AddTo(Disposable);
SelectedItem.AddTo(Disposable);
List<string> names = ["アイテム1", "アイテム2", "アイテム3"];
foreach(string name in names)
{
ListViewItem item = new()
{
Name = name,
};
item.ContextMenuItems.Add(new MenuItemViewModel(
"メッセージボックス",
() => { Execute(); }
));
Items.Add( item );
}
}
public void Execute()
{
if (SelectedItem.Value is null) return;
MessageBox.Show($"{SelectedItem.Value.Name}", "選択されているアイテム");
}
}
ファイル名:MenuItemViewModel.cs
using System.ComponentModel;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;
namespace ListViewOnContextMenu01;
public class MenuItemViewModel: INotifyPropertyChanged, IDisposable
{
#region
// INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
// IDisposable
private CompositeDisposable Disposable { get; } = [];
public void Dispose() => Disposable.Dispose();
#endregion
public string Header { get; set; }
public ReactiveCommand Command { get; } = new();
// コンストラクタ
public MenuItemViewModel(string header, Action execute)
{
Command.AddTo(this.Disposable);
Header = header;
Command.Subscribe(_ => execute());
}
}
ファイル名:ViewModelCleanupBehavior.cs
using Microsoft.Xaml.Behaviors;
using System.Windows;
namespace ListViewOnContextMenu01;
public class ViewModelCleanupBehavior : Behavior<Window>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.Closed += this.WindowClosed;
}
private void WindowClosed(object? sender, EventArgs e)
{
(this.AssociatedObject.DataContext as IDisposable)?.Dispose();
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.Closed -= this.WindowClosed;
}
}
解説
ListView上でコンテキストメニューが表示されるようにXAMLを記述します。
コンテキストメニューはVisualTreeに含まれないので、DataContextにPlacementTarget.DataContextを指定することでListViewItemをデータソースにする仕掛けになっています。
DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}"
データソースとなる、ListViewItemにコンテキストメニューをプロパティとして持たせます。
public ReactiveCollection<MenuItemViewModel> ContextMenuItems { get; set; } = [];
動的にメニュー項目を登録する目的で、コレクションにしています。
ListViewのアイテムを追加する際にコンテキストメニューの項目を生成し追加します。
item.ContextMenuItems.Add(new MenuItemViewModel(
"メッセージボックス",
() => { Execute(); }
));
まとめ
コンテキストメニュー項目を動的に追加する方法を知っておけば、状況に応じて必要な項目のみを表示するなど、細かな制御が出来ると思います。
コメント