1
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DataGridのSelectedItemsがbind出来ない!!

Last updated at Posted at 2020-02-21

0. はじめに

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

20.02.21 17:55 JST 追記
やっぱり仮想化をオフしないと、連動しませんでした。
具体的には、視界外のIsSelectedtrueにしても、無視されるようです。
解決策が見つかれば、記事を更新します。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に書いてそれを継承するようにします。

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を作ります。

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に設定します。

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を作ります。

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
xamlの名前空間指定は、次のようになります。

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

6. サンプルコード

まずは、Viewから。

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は、
            <DataGrid.RowStyle>
                <Style TargetType="{x:Type DataGridRow}">
                    <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWay}"/>
                </Style>
            </DataGrid.RowStyle>
  • View → ViewModelのIsSelectedSelectedItemsCountのbindは、
            <i:Interaction.Behaviors>
                <local:NotifyIsSelectedToSouceBehavior/>
                <local:BindSelectedItemsCountBehavior SelectedItemsCount="{Binding SelectedItemsCount, Mode=OneWayToSource}"/>
            </i:Interaction.Behaviors>

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

次に、ViewModelです。

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
選択状態を変化させると、上端の選択数と、左端桁のTrue/Falseが変化するのが見て取れます。

8. とはいえ、SelectedItemsが欲しいよね

という方は、次の記事を参考にどうぞ。
リンク : WPFのDataGridで選択された複数のアイテムをバインドするためのビヘイビア

1
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?