0. はじめに
Freeradicalの中の人、yamarahです。
普段は、Autodesk InventorのAddIn作成に関する記事を書いています。
今回は、WPFのDataGrid
において、複数選択を許可した場合に、選択状態をどのようにViewModelにbindするかという記事です。
WPF自体は詳しくないので、おかしな点があればご指摘歓迎です。
20.02.21 17:55 JST 追記
やっぱり仮想化をオフしないと、連動しませんでした。
具体的には、視界外のIsSelected
をtrue
にしても、無視されるようです。
解決策が見つかれば、記事を更新します。work aroundがあれば、よろおねです。
1. 方針
まず前提として、DataGrid.SelectedItems
はbindできません。単選択なら問題になりませんが、複数選択の状態を知るには少し手間が必要です。
では、方針ですが、概ね次の記事に従います。
リンク : ListViewの選択項目を取得/設定する
つまりは、
ListViewItem.IsSelected に対するバインディングは OneWay (VM -> Vのみ)にする
ListView.SelectedItems の内容は一切信用しない
VからVMへの反映は、SelectionChanged イベントで自力でやる
ということです。
SelectedItems
のようなCollectionを用意するのではなく、各Item内のIsSelected
を選択状態と同期するようになります。
ただし、折角だからコードビハインドは嫌だよね、ってことで、Behaviorを使って実装します。
2. Item側の準備
Behaviorに汎用性を持たせたいので、IsChecked
はData型に直接書くのではなく、基底classに書いてそれを継承するようにします。
using System.ComponentModel;
namespace TestApp
{
public class CollectionItemWithIsSelectedProperty : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged = null;
private bool _IsSelected;
public bool IsSelected
{
get => _IsSelected;
set
{
if (_IsSelected != value)
{
_IsSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
}
}
}
}
}
では、これを継承したテスト用のclassを作ります。
using System.IO;
namespace TestApp
{
class PathInfo : CollectionItemWithIsSelectedProperty
{
public string Directory { get; private set; }
public string FileName { get; private set; }
public PathInfo(string fullFileName)
{
Directory = Path.GetDirectoryName(fullFileName);
FileName = Path.GetFileName(fullFileName);
}
}
}
3. View → ViewModelのBehavior
SelectionChanged
をhookして、選択状態の変化を各Itemに設定します。
using Microsoft.Xaml.Behaviors;
using System.Windows.Controls;
namespace TestApp
{
/// <summary>
/// 選択状態をItem.IsSelectedに反映するBehavior
/// </summary>
class NotifyIsSelectedToSouceBehavior : Behavior<DataGrid>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.SelectionChanged += DataGrid_SelectionChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.SelectionChanged -= DataGrid_SelectionChanged;
}
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (CollectionItemWithIsSelectedProperty addedItem in e.AddedItems)
{
addedItem.IsSelected = true;
}
foreach (CollectionItemWithIsSelectedProperty removedItem in e.RemovedItems)
{
removedItem.IsSelected = false;
}
}
}
}
4. SelectedItemsCountのBehavior
やっぱり、選択数は欲しいですよね。ItemsSourceをなめてIsSelected
を数えるのは非効率なので、これもBehaviorを作ります。
using Microsoft.Xaml.Behaviors;
using System.Windows;
using System.Windows.Controls;
namespace TestApp
{
/// <summary>
/// DataGrid内のSelectedItems.Countに相当する値を返すBehavior
/// </summary>
public class BindSelectedItemsCountBehavior : Behavior<DataGrid>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += DataGrid_SelectionChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectionChanged -= DataGrid_SelectionChanged;
base.OnDetaching();
}
public static DependencyProperty SelectedItemsCountProperty =
DependencyProperty.Register("SelectedItemsCount", typeof(int), typeof(BindSelectedItemsCountBehavior), new PropertyMetadata(null));
public int SelectedItemsCount
{
get { return (int)GetValue(SelectedItemsCountProperty); }
set { SetValue(SelectedItemsCountProperty, value); }
}
void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedItemsCount += e.AddedItems.Count;
SelectedItemsCount -= e.RemovedItems.Count;
}
}
}
5. Interaction.Behaviorsを使えるようにする
さて、いよいよxamlに記載・・・の前に、xaml内にBehaviorを記述するのに準備が必要です。
ググると、BlendのSDKを・・・という記事がヒットしますが、VS2019では勝手が違うので注意。
リンク : 新しい Behavior ライブラリへの更新 #27
xamlの名前空間指定は、次のようになります。
<Window ・・・続く
xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;
assembly=Microsoft.Xaml.Behaviors"
6. サンプルコード
まずは、Viewから。
<Window x:Class="TestApp.TestView"
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:local="clr-namespace:TestApp"
mc:Ignorable="d"
Title="TestView" Height="300" Width="300">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Text="{Binding SelectedItemsCountReport}"/>
<DataGrid ItemsSource="{Binding PathInfoList, Mode=OneWay}" SelectionMode="Extended" IsReadOnly="True"
GridLinesVisibility="None" HeadersVisibility="Column" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Width="SizeToHeader" Header="IsSelected" Binding="{Binding IsSelected}"/>
<DataGridTextColumn Width="*" Header="パス" Binding="{Binding Directory}"/>
<DataGridTextColumn Width="SizeToCells" MinWidth="70" Header="ファイル名" Binding="{Binding FileName}"/>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWay}"/>
</Style>
</DataGrid.RowStyle>
<i:Interaction.Behaviors>
<local:NotifyIsSelectedToSouceBehavior/>
<local:BindSelectedItemsCountBehavior SelectedItemsCount="{Binding SelectedItemsCount, Mode=OneWayToSource}"/>
</i:Interaction.Behaviors>
</DataGrid>
</DockPanel>
</Window>
- ViewModel → Viewの
IsSelected
のbindは、
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWay}"/>
</Style>
</DataGrid.RowStyle>
- View → ViewModelの
IsSelected
とSelectedItemsCount
のbindは、
<i:Interaction.Behaviors>
<local:NotifyIsSelectedToSouceBehavior/>
<local:BindSelectedItemsCountBehavior SelectedItemsCount="{Binding SelectedItemsCount, Mode=OneWayToSource}"/>
</i:Interaction.Behaviors>
です。bind方向の指定は、上記の通りに忘れず設定してください。せっかくコードを書いたんだから、SelectedItemsCount
のModeのDefault値をOneWayToSource
にしたかったのですが、簡単には出来ないようなので諦めました。
次に、ViewModelです。
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace TestApp
{
class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public ObservableCollection<PathInfo> PathInfoList { get; private set; }
private int _SelectedItemsCount;
public int SelectedItemsCount
{
get => _SelectedItemsCount;
set
{
if (_SelectedItemsCount != value)
{
_SelectedItemsCount = value;
SelectedItemsCountReport = $"SelectedItemsCount : {_SelectedItemsCount}";
}
}
}
private string _SelectedItemsCountReport = string.Empty;
public string SelectedItemsCountReport
{
get => _SelectedItemsCountReport;
set
{
if (_SelectedItemsCountReport != value)
{
_SelectedItemsCountReport = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItemsCountReport)));
}
}
}
public TestViewModel()
{
PathInfoList = new ObservableCollection<PathInfo>
{
new PathInfo(@"C:\temp\file0001.txt") { IsSelected = true },
new PathInfo(@"C:\temp\file0002.txt") { IsSelected = true },
new PathInfo(@"C:\temp\file0003.txt") { IsSelected = false },
new PathInfo(@"C:\temp\file0004.txt") { IsSelected = true },
new PathInfo(@"C:\temp\file0005.txt") { IsSelected = false }
};
}
}
}
実際のアプリケーションでは、SelectedItemsCountにReactivePropertyなどを使うことになるでしょう。
7. 動作結果
では、これらのコードでWindowを表示してみましょう。(スタートアップのコードは、各自でどうぞ)
選択状態を変化させると、上端の選択数と、左端桁のTrue
/False
が変化するのが見て取れます。
8. とはいえ、SelectedItems
が欲しいよね
という方は、次の記事を参考にどうぞ。
リンク : WPFのDataGridで選択された複数のアイテムをバインドするためのビヘイビア