DataGridのSelectedItemsをどんな時でも取得する方法

C#

こんにちは。働く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には
いくつか仕様面で違いがあるので気を付けましょう。

コメント

タイトルとURLをコピーしました