こんにちは。働くC#プログラマーのさんさめです。
WPFにおけるDataGridには
SelectedItem, SelectedItemsという選択している行の
DataContextにアクセスできるプロパティがあります。
しかし、このプロパティ、
実はSelectionUnitをCellやCellOrRowHeaderに変更していると、
正常に取得できないことがあります。
セル単位で選択状態にしている場合は、
SelectedItemsはからっぽ…
(※詳細については
【WPF】SelectedItemsとSelectedCellsの違い。使い分けが重要
で解説しています)
そこで、本記事ではBehaviorを活用して、
SelectionUnitに関わらずSelectedItemsに相当する要素を
Bindingする方法について解説します。
前提:Behaviorを使うために参照を追加する
そもそもBehaviorって何?
って方は、WPFのビヘイビアという記事で
詳しく解説されているので先にご覧ください。
今回は、ビヘイビアクラスを用います。
さて、VisualStudio2019環境では、ビヘイビアを使うためには
Microsoft.Xaml.Behaviors.Wpf に参照を張らないといけないので、
さっそくやっていきましょう。
まずはNugetパッケージを開いて…
Microsoft.Xaml.Behaviors.Wpfを選択してインストールします。
確認ダイアログがでますが、そのままOKを押して大丈夫です。
BehaviorでSelectedCellsから選択行を取得する
以下のようなビヘイビアクラスを作ります。
using Microsoft.Xaml.Behaviors;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace DataGridSelectedItem
{
public class DataGridSelectedItemsProxyBehavior : Behavior<DataGrid>
{
public IEnumerable<object> SelectedItemsProxy
{
get { return (IEnumerable<object>)GetValue(SelectedItemsProxyProperty); }
set { SetValue(SelectedItemsProxyProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProxyProperty =
DependencyProperty.Register(nameof(SelectedItemsProxy),
typeof(IEnumerable<object>),
typeof(DataGridSelectedItemsProxyBehavior),
new FrameworkPropertyMetadata(Enumerable.Empty<object>(),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedCellsChanged += OnSelectedCellsChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SelectedCellsChanged -= OnSelectedCellsChanged;
}
private void OnSelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
this.SetCurrentValue(SelectedItemsProxyProperty,
AssociatedObject.SelectedCells
.Where(x => x.IsValid)
.Select(x => x.Item)
.Distinct());
}
}
}
やっていることは単純です。
どんな時でも選択しているセルが取得できる、
SelectedCellsプロパティと、
選択しているセルが変更されたときに発火される
SelectedCellsChangedイベントを活用します。
SelectedCellsからはItemプロパティによって、
行のDataContextが取得できるため、
それをビヘイビアクラス自体のDependencyPropertyである
SelectedItemsProxyプロパティに流しています。
Behaviorクラスの実際の使い方
実際の使い方は以下の通りです。
使う側でも、
Microsoft.Xaml.Behaviors.Wpf に参照を張る必要があるので、
プロジェクトを分けている場合は気を付けてください。
本来のSelectedItemsプロパティとの差分を見たいため、
少し冗長なコードになっています。
<Window x:Class="DataGridSelectedItem.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:DataGridSelectedItem"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<DockPanel>
<DockPanel DockPanel.Dock="Left"
Width="120">
<StackPanel DockPanel.Dock="Top"
MinHeight="200"
Background="WhiteSmoke">
<TextBlock Text="素のSelectedItems"/>
<ItemsControl ItemsSource="{Binding SelectedItems, ElementName=MainDataGrid}"
Margin="4"/>
</StackPanel>
<StackPanel Background="LightGray">
<TextBlock Text="SelectedItemsProxy"/>
<ItemsControl ItemsSource="{Binding SelectedItemsProxy, ElementName=ProxyBehavior}"
Margin="4"/>
</StackPanel>
</DockPanel>
<DataGrid x:Name="MainDataGrid"
SelectionUnit="CellOrRowHeader"
CanUserAddRows="False"
CanUserDeleteRows="False"
RowHeaderWidth="20"
ItemsSource="{Binding People}">
<i:Interaction.Behaviors>
<local:DataGridSelectedItemsProxyBehavior
x:Name="ProxyBehavior"
SelectedItemsProxy="{Binding SelectedItems, Mode=OneWayToSource}"/>
</i:Interaction.Behaviors>
</DataGrid>
</DockPanel>
</Window>
ViewModelとBindingするために特に重要なのはこの部分ですね。
<i:Interaction.Behaviors>
<local:DataGridSelectedItemsProxyBehavior
x:Name="ProxyBehavior"
SelectedItemsProxy="{Binding SelectedItems, Mode=OneWayToSource}"/>
</i:Interaction.Behaviors>
ViewModel層に実際に選択行を流したい場合は、
ModeをOneWayToSourceにしておくのを忘れないようにしましょう。
もちろんTwoWayでも構いませんが、
ViewModel層から選択行をコントロールしたいことはないと思うので
ビヘイビアクラス側がそもそも未実装です。
ViewModelでの取得方法は以下のようになります。
public class MainWindowViewModel
{
public List<Person> People { get; }
= new List<Person>();
// Binding用のプロパティ
public IEnumerable<object> SelectedItems { get; set; }
// 実際に取得する際はこちらのプロパティを用いる
public IEnumerable<Person> SelectedPeople
=> SelectedItems?.
OfType<Person>();
public MainWindowViewModel()
{
People.Add(new Person
{
Name = "Foo",
Age = 20,
});
People.Add(new Person
{
Name = "Bar",
Age = 42,
});
}
}
ViewModelの方は比較的すっきりしてますね。
ちょっと工夫があり、
Bindingに用いているプロパティと
実際に扱う際のプロパティを分けています。
ViewModel層では要素のクラスの列挙として
扱いたいことがほとんどだと思うので、
あらかじめOfType<Person>しておくプロパティを用意しておくわけです。
これなら万が一要素のクラスが変わった時にも、
コードの変更点が少なく済みます。
実行結果で素のSelectedItemsと見比べてみる
実際に実行してみて、
挙動の差を見比べてみましょう。
まずは、行ヘッダをクリックして行選択してみます。
この場合は、どちらのプロパティでも
ちゃんと取得できていることがわかります。
次に、セルだけを選択してみます。
すると、素のSelectedItemsプロパティでは、
要素が取得できていませんが、
SelectedItemsProxyの方では
ちゃんと取得できていることが分かります。
まとめ
DataGridにおいてSelectionUnitをFullRow以外にしても、
正常にSelectedItemsを取得する方法について解説しました。
- SelectedCells経由でSelectedItemsを構築する
Behaviorクラスを作る - ViewModel層ではBinding用のセッターを公開するだけ
- 実際に扱うときはOfTypeしておくプロパティを用意しておくと便利
これで、
ユーザビリティの高いSelectionUnit=”CellOrRowHeader”を、
気軽に使うことができます。
最後までお読みいただきありがとうございました。
関連記事
関連記事です。
DataGridをMVVMで活用するためには、
SelectionUnitの他にも、
既定から変更しておくべきプロパティがあります。
SelectedItemsとSelectedCellsには
いくつか仕様面で違いがあるので気を付けましょう。
コメント