C#のWPFでリストビューでアイテムの選択とコンテキストメニュー

コンピュータ

リストビュー上でマウスをクリックし、ビューモデル側で該当するアイテムを検出する方法を考えてみます。

ソースコード

ファイル名:FileEntity.cs

namespace ListViewOnClick;

public class FileEntity
{
    public string Name { get; set; } = "";
    public string FullName { get; set; } = "";
 }

ファイル名:MainWindow.xaml

<Window x:Class="ListViewOnClick.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:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors"
        xmlns:interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
        xmlns:local="clr-namespace:ListViewOnClick"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <interactivity:EventToReactiveCommand Command="{Binding WindowLoadedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <ListView
            x:Name="FilesListView"
            ItemsSource="{Binding Files}"
            SelectedIndex="{Binding FilesListViewSelectedIndex.Value}">
            <ListView.Resources>
                <Style x:Key="listviewHeaderStyle" TargetType="GridViewColumnHeader">
                    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
                </Style>
            </ListView.Resources>
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseDoubleClick">
                    <interactivity:EventToReactiveCommand Command="{Binding FilesListViewDoubleClickCommand}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
            <ListView.ContextMenu>
                <ContextMenu>
                    <MenuItem Header="メニュー1" Command="{Binding FilesListViewMenu1Command}" />
                </ContextMenu>
            </ListView.ContextMenu>
            <ListView.View>
                <GridView ColumnHeaderContainerStyle="{StaticResource listviewHeaderStyle}">
                    <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.ComponentModel;
using Reactive.Bindings;

using System.Diagnostics;
using System.IO;

namespace ListViewOnClick;

public class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged(string name) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    public ReactiveCommand<EventArgs> WindowLoadedCommand { get; }
    public ReactiveCollection<FileEntity> Files { get; set; } = [];
    public ReactiveProperty<int> FilesListViewSelectedIndex { get; set; } = new();
    public ReactiveCommand<EventArgs> FilesListViewDoubleClickCommand { get; } = new();
    public ReactiveCommand<EventArgs> FilesListViewMenu1Command { get; } = new();
    public MainWindowViewModel()
    {
        WindowLoadedCommand = new ReactiveCommand<EventArgs>()
            .WithSubscribe(e =>
            {
                string targetDir = @"H:\csharp\dotnet8\wpf\ListViewOnClick";
                foreach(var fullname in Directory.EnumerateFileSystemEntries(targetDir))
                {
                    Files.AddOnScheduler(
                        new FileEntity()
                        {
                            Name = Path.GetFileName(fullname),
                            FullName = fullname,
                        }
                    );
                }
            }
        );
        FilesListViewSelectedIndex
            .Subscribe(e=>
            {
                // リストビューの選択が変更されると実行される。
                int index = e;
                Debug.Print($"選択されたインデックス:{index}");
            }
        );
        FilesListViewDoubleClickCommand
            .Subscribe(e=>
            {
                // ダブルクリックイベント
                int index = FilesListViewSelectedIndex.Value;
                Debug.Print($"ダブルクリックインデックス:{index}");
            }
        );
        FilesListViewMenu1Command
            .Subscribe(e=>
            {
                // コンテキストメニュー1を実行
                int index = FilesListViewSelectedIndex.Value;
                Debug.Print($"コンテキストメニュー1インデックス:{index}");
            }
        );
    }
}

選択された場合

ListViewのSelectedIndexにビューモデルのプロパティをバインディングし、購読することで選択アイテムの変更を捕まえます。
ちなみに複数選択は考慮していません。あと選択されていない場合-1がセットされるようです。

ダブルクリック

ListViewのDobuleClickイベントをEventToReactiveCommandでビューモデルのコマンドへつなげてみました。

右クリック(コンテキストメニュー)

ListViewにコンテキストメニューを追加してみました。メニューアイテムからビューモデルのコマンドを呼び出します。

問題点

ListViewに仕掛けをしているのでアイテムとは関係ないヘッダーや未選択領域でイベント発生させても反応してしまいます。
また、ダブルクリックや右クリックのアイテムの特定マウスの位置とは官憲なくSelectedIndexの値を使っており、ユーザーの意図しない動作になりそうな感じがします。

コメント