DataGridのSelectedItemsがbind出来ない!!

# 0. はじめに

[Freeradical](https://www.freeradical.jp)の中の人、[yamarah](https://qiita.com/yamarah)です。
普段は、[Autodesk InventorのAddIn作成に関する記事](https://qiita.com/yamarah/items/cee08eb8f2b7aecbda83)を書いています。
今回は、WPFの`DataGrid`において、複数選択を許可した場合に、選択状態をどのようにViewModelにbindするかという記事です。
WPF自体は詳しくないので、おかしな点があればご指摘歓迎です。

20.02.21 17:55 JST 追記
やっぱり仮想化をオフしないと、連動しませんでした。
具体的には、視界外の`IsSelected`を`true`にしても、無視されるようです。
解決策が見つかれば、記事を更新します。work aroundがあれば、よろおねです。

# 1. 方針
まず前提として、`DataGrid.SelectedItems`はbindできません。単選択なら問題になりませんが、複数選択の状態を知るには少し手間が必要です。
では、方針ですが、概ね次の記事に従います。

リンク : [ListViewの選択項目を取得/設定する](https://qiita.com/wonderful_panda/items/36bf500094cc42f7ea97)

つまりは、

>ListViewItem.IsSelected に対するバインディングは OneWay (VM -> Vのみ)にする
>ListView.SelectedItems の内容は一切信用しない
>VからVMへの反映は、SelectionChanged イベントで自力でやる

ということです。
`SelectedItems`のようなCollectionを用意するのではなく、各Item内の`IsSelected`を選択状態と同期するようになります。
ただし、折角だからコードビハインドは嫌だよね、ってことで、Behaviorを使って実装します。

# 2. Item側の準備
Behaviorに汎用性を持たせたいので、`IsChecked`はData型に直接書くのではなく、基底classに書いてそれを継承するようにします。

```c#:CollectionItemWithIsSelectedProperty.cs
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を作ります。

```c#:PathInfo.cs
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に設定します。

```c#:NotifyIsSelectedToSouceBehavior.cs
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を作ります。

```c#:BindSelectedItemsCountBehavior.cs
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](https://github.com/runceel/Livet/issues/27)
xamlの名前空間指定は、次のようになります。

```xml
<Window ・・・続く
        xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;
            assembly=Microsoft.Xaml.Behaviors" 
```

# 6. サンプルコード
まずは、Viewから。

```xml:TestView.xml
<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は、

```xml
            <DataGrid.RowStyle>
                <Style TargetType="{x:Type DataGridRow}">
                    <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWay}"/>
                </Style>
            </DataGrid.RowStyle>
```

* View → ViewModelの`IsSelected`と`SelectedItemsCount`のbindは、

```xml
            <i:Interaction.Behaviors>
                <local:NotifyIsSelectedToSouceBehavior/>
                <local:BindSelectedItemsCountBehavior SelectedItemsCount="{Binding SelectedItemsCount, Mode=OneWayToSource}"/>
            </i:Interaction.Behaviors>
```

です。bind方向の指定は、上記の通りに忘れず設定してください。せっかくコードを書いたんだから、`SelectedItemsCount`のModeのDefault値を`OneWayToSource`にしたかったのですが、簡単には出来ないようなので諦めました。

次に、ViewModelです。

```c#:TestViewModel.cs
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を表示してみましょう。(スタートアップのコードは、各自でどうぞ)
![TestWindow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424028/1f754fdb-6e24-3bf1-b79c-313677b52ca2.png)
選択状態を変化させると、上端の選択数と、左端桁の`True`/`False`が変化するのが見て取れます。

# 8. とはいえ、`SelectedItems`が欲しいよね
という方は、次の記事を参考にどうぞ。
リンク : [WPFのDataGridで選択された複数のアイテムをバインドするためのビヘイビア](http://chorusde.hatenablog.jp/entry/2013/02/28/064747)