WPFのListViewでListViewItemを編集する方法をさがす

コンピュータ

エクスプローラーF2キーでのファイル名が編集が出来るますが、ListViewで同じことができないか試行錯誤しています。

プロジェクトの作成

ソースコード

ファイル名:BooleanToVisibilityConverter.cs

using System.Windows;
using System.Windows.Data;

namespace ListItemEdit01;
public class BooleanToVisibilityConverter : IValueConverter
{
    public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        bool visibility = (bool)value;
        string? parameterString = parameter as string;
        bool reverse = (parameterString != null && parameterString.ToLower() == "collapsed");
        return (visibility ^ reverse) ? Visibility.Visible : Visibility.Collapsed;
    }

    public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value is Visibility.Visible;
    }
}

boolをVisibilityへ変換するコンバータ。
ファイル名:FocusBehavior.cs

using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;

namespace ListItemEdit01;

public static class FocusBehavior
{
    public static readonly DependencyProperty IsFocusedProperty =
        DependencyProperty.RegisterAttached(
            "IsFocused",
            typeof(bool),
            typeof(FocusBehavior),
            new PropertyMetadata(false, OnIsFocusedChanged));

    public static bool GetIsFocused(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsFocusedProperty);
    }

    public static void SetIsFocused(DependencyObject obj, bool value)
    {
        obj.SetValue(IsFocusedProperty, value);
    }

    private static void OnIsFocusedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is TextBox textBox && (bool)e.NewValue)
        {
            // Dispatcher.BeginInvoke を使用して、UI スレッドで非同期的にフォーカスを設定
            textBox.Dispatcher.BeginInvoke((Action)(() =>
            {
                textBox.Focus();
                //Keyboard.Focus(textBox); // 必要に応じてキーボードフォーカスも設定
                //SetIsFocused(textBox, false); // フォーカス移動後にリセット
            }), DispatcherPriority.Loaded);
        }
    }
}

編集用のテキストボックスにフォーカスを移動するためのビヘイビア。
ファイル名:ItemViewModel.cs

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

namespace ListItemEdit01;
public class ItemViewModel : 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 ReactiveProperty<string> Name {get; set;} = new("");
    public ReactiveProperty<bool> IsEditing {get; set;} = new(false);

    public ItemViewModel()
    {
        Name.AddTo(Disposable);
        IsEditing.AddTo(Disposable);
    }
}

こちらがListViewItemにバインドされます。
ファイル名:MainWindow.xaml

<Window x:Class="ListItemEdit01.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:ListItemEdit01"
    mc:Ignorable="d"
    Title="Title"
    Height="450"
    Width="800"
    
    xmlns:interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
    xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors">
  <Window.DataContext>
    <local:MainWindowViewModel x:Name="Root"/>
  </Window.DataContext>
    <Window.Resources>
        <local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
    </Window.Resources>
  <i:Interaction.Behaviors>
    <local:ViewModelCleanupBehavior />
  </i:Interaction.Behaviors>
  <Grid>
    <ListView ItemsSource="{Binding Items}"
        SelectedItem="{Binding SelectedItem.Value}">
        <ListView.InputBindings>
            <KeyBinding Key="F2" Command="{Binding ElementName=Root, Path=EditItemCommand}" />
            <KeyBinding Key="Esc" Command="{Binding ElementName=Root, Path=LostFocusCommand}" />
        </ListView.InputBindings>
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="MouseDoubleClick">
                <interactivity:EventToReactiveCommand Command="{Binding EditItemCommand}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
        <ListView.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <TextBlock Text="{Binding Name.Value}" Visibility="{Binding IsEditing.Value, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Collapsed}">
                    </TextBlock>
                    <TextBox Text="{Binding Name.Value, UpdateSourceTrigger=LostFocus}" local:FocusBehavior.IsFocused="{Binding IsEditing.Value}" Visibility="{Binding IsEditing.Value, Converter={StaticResource BooleanToVisibilityConverter}}">
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="LostFocus">
                                <interactivity:EventToReactiveCommand Command="{Binding ElementName=Root, Path=LostFocusCommand}" />
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                        <TextBox.InputBindings>
                            <KeyBinding Key="Enter" Command="{Binding ElementName=Root, Path=LostFocusCommand}" />
                        </TextBox.InputBindings>
                    </TextBox>
                </Grid>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
  </Grid>
</Window>

MainWindowのViewになります。

ファイル名:MainWindowViewModel.cs

using System.Diagnostics;
using System.ComponentModel;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;
using System.Windows;

namespace ListItemEdit01;
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<ItemViewModel> Items { get; set; } = [];
    public ReactiveProperty<ItemViewModel> SelectedItem { get; set; } = new();
    ItemViewModel? PreviousSelectItem;
    public ReactiveCommand<EventArgs> EditItemCommand { get; } = new();
    public ReactiveCommand<EventArgs> LostFocusCommand { get; } = new();

    public MainWindowViewModel()
    {
        Items.AddTo(this.Disposable);

        Items.AddOnScheduler(new() { Name = new("1アイテム"), });
        Items.AddOnScheduler(new() { Name = new("Bアイテム"), });
        Items.AddOnScheduler(new() { Name = new("Eアイテム"), });
        Items.AddOnScheduler(new() { Name = new("Fアイテム"), });
        Items.AddOnScheduler(new() { Name = new("Sアイテム"), });
        Items.AddOnScheduler(new() { Name = new("Xアイテム"), });

        EditItemCommand.Subscribe(e=>
        {
            if (SelectedItem is null) return;

            SelectedItem.Value.IsEditing.Value = true;
        }).AddTo(Disposable);
        LostFocusCommand.Subscribe(e=>
        {
            if (SelectedItem is null) return;

            SelectedItem.Value.IsEditing.Value = false;
        }).AddTo(Disposable);

        SelectedItem.Subscribe(e=>
        {
            if (PreviousSelectItem is not null && PreviousSelectItem.IsEditing.Value == true)
            {
                PreviousSelectItem.IsEditing.Value = false;
            }
            PreviousSelectItem = e;
        }).AddTo(Disposable);
    }
}

MainWindowのデータコンテキスト

ファイル名:ViewModelCleanupBehavior.cs

using Microsoft.Xaml.Behaviors;
using System.Windows;

namespace ListItemEdit01;
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;
    }
}

ウィンドウを閉じる(アプリの終了)際、オブジェクトを一括Dispose()呼び出し用

実行

dotnet run

起動するとリストビューが表示される。

F2キーを押すと編集モードになります。

編集変更後エンターキーを押す。

キー割当

エクスプローラー準拠を目指しましたが、結果は以下の通りになりました。

F2
編集モード
Esc
編集モード解除
ダブルクリック
編集モード
Enter
編集モード解除

Escはキャンセルではなく単純に編集モードの終了になっています。

仕組み

ListViewItemの項目としてTextBlockとTextBoxを用意し、同じName.Valueとバインドしています。IsEditing.ValueをフラグとしてTextBlockを表示モードとTextBox編集モードの表示・非表示を切り替えています。

コメント