【WPF】DataTableバインドなDataGridでSelect Allチェックボックスを作る
こういうのがやりたかったけど、なかなかちゃんとした例が見つからず大変だったので、なんとかできた動くやつの内容を解説します。
githubにコードあげてます。
全体の流れはこんな感じです。
・ヘッダーにチェックボックスを表示
・SelectAllプロパティの実装
・ワンクリックで反応するセルのチェックボックスの作成
・セルの変更をSelectAllチェックボックスに通知
ヘッダーにチェックボックスを表示
とりあえずヘッダーにチェックボックスを表示したいです。
ぐぐると予めデータの型がわかってる場合にXAML側でDataGridTemplateColumnを用意するパターンが出てくるのですが、今回はDataTableを使って任意の型のデータに対応したいです。
XAML側で自動的に対応させる方法がわからなかったので
「〇〇という名前の列が追加されたらXAMLに用意しといたテンプレートを使う」
という処理をスクリプトで行う作戦にします。
XAMLにテンプレートを用意
<DataGrid Name="MyDataGrid" ItemsSource="{Binding Items}">
<DataGrid.Resources>
<DataTemplate x:Key="SelectAll">
<CheckBox IsChecked="{Binding DataContext.SelectAll, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}" />
</DataTemplate>
</DataGrid.Resources>
</DataGrid>
DataGrid内のリソースとして用意しときます。
IsCheckedを親のDataGrid.DataContextにバインドしたクラスのSelectAllプロパティとバインドしておきます。
列追加時のフック
DataGrid.AutoGeneratingColumnイベントで行えます。DataGrid.AutoGenerateColumns == false だと呼ばれないので注意。
protected virtual string headerTemplateKey => "SelectAll";
~
dataGrid.AutoGeneratingColumn += (s, e) =>
{
if (e.PropertyName == selectedPropertyName)
{
var c = new DataGridTemplateColumn()
{
Header = e.Column.Header,
HeaderTemplate = (DataTemplate)dataGrid.Resources[headerTemplateKey],
HeaderStringFormat = e.Column.HeaderStringFormat,
SortMemberPath = e.PropertyName
};
e.Column = c;
}
};
プロパティの名前からSelectAllの列か判定しオレオレDataGridTemplateColumnを作ります。HeaderTemplateに用意したテンプレートをDataGrid.Resource経由で参照しセットします。
これでヘッダにチェックボックスが表示され、値を変えるとDataGrid.DataContextにバインドしたクラスのSelectAllプロパティが呼ばれます。
SelectAllプロパティの実装
public DataTable Items { get; set; }
protected virtual string selectedPropertyName => "IsSelected";
~
public bool? SelectAll
{
get
{
var uniqueList = Items.AsEnumerable().Select(row => row[selectedPropertyName]).Distinct().ToList();
return uniqueList.Count() == 1 ? (bool?)uniqueList.First() : null;
}
set
{
Items.AsEnumerable().ToList().ForEach(row => row[selectedPropertyName] = value);
}
}
SelectAll内ではDataTableを舐める挙動を適当に実装しておきます。今回はIsSelectedという名前の列を対象してしています。
bool?型を返しておりtrue,falseが混在するときはnullにしておくとチェックボックスが四角い表示になります。
セルにチェックボックスを表示
DataTableにbool値の列を定義すればセルの表示は勝手にチェックボックになって便利、、、
var dt = new DataTable();
dt.Columns.Add("IsSelected", typeof(bool));
じゃなかった!!!
1度目のクリックで選択、2度目のクリックでチェックボックス操作、と2回クリックが必要な挙動になってます。
わざわざこのような挙動になるDataGridViewCheckBoxColumnになっているようです。
たしかに他の型のセルもそのような挙動なので一貫してはいるのですが、見えているチェックボックスがクリック一発で反応しないのは触り心地にかなり違和感があります。やはりワンクリックにしたいところ。
ワンクリックで反応するセルのチェックボックス作成
ヘッダーと同じように、生のチェックボックスが入るセルをXAML側で用意しておき、列追加時に指定する作戦で行きます。
<DataGrid Name="MyDataGrid" ItemsSource="{Binding Items}" >
<DataGrid.Resources>
<DataTemplate x:Key="SelectAll">
<CheckBox IsChecked="{Binding DataContext.SelectAll, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}" />
</DataTemplate>
<DataTemplate x:Key="IsSelected">
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGrid.Resources>
</DataGrid>
IsCheckedの設定が少し複雑になっています。
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged
UIからの変更も、SelectAllからスクリプト経由での変換もどちらも受け付けるのでこれらのオプションが必要になります。
スクリプト側はHeaderTemplateをセットするところにあいのりします。
protected virtual string cellTemplateKey => "IsSelected";
~
dataGrid.AutoGeneratingColumn += (s, e) =>
{
if (e.PropertyName == selectedPropertyName)
{
var c = new DataGridTemplateColumn()
{
CellTemplate = (DataTemplate)dataGrid.Resources[cellTemplateKey],
Header = e.Column.Header,
HeaderTemplate = (DataTemplate)dataGrid.Resources[headerTemplateKey],
HeaderStringFormat = e.Column.HeaderStringFormat,
SortMemberPath = e.PropertyName // this is used to index into the DataRowView so it MUST be the property's name (for this implementation anyways)
};
e.Column = c;
}
};
CellTemplateにDataGrid.ResouceからとってきたXAMLのテンプレートをセットします。これでワンクリックで反応するチェックボックのセルができました。
セルの変更をSelectAllチェックボックスに通知
最後にIsSelected列のいずれかの値に変更があった場合SelectAllチェックボックスの表示が切り替わるようにします。
DataTableの値変更イベントをフックしてPropertyChangedイベントを発行する
というやり方になります。
まずはDataGrid.DataContextにバインドしているクラスをINotifyPropertyChangedにしておき、PropertyChangedイベントを呼べるようにしておきます。
public event PropertyChangedEventHandler PropertyChanged;
~
Items.ColumnChanged += (s, e) =>
{
if (e.Column.ColumnName == selectedPropertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("SelectAll"));
}
};
あとはDataTable.ColumnChangedイベントにフックしselectedPropertyNameの列が変更されたら、PropertyChangedイベントを発行します。
まとめ
こんな感じでなんとか動作するものができました。記事中のコード片だとちょっと分かりづらい気がするので、具体的にはgithubのプロジェクトを落として見てもらうのがいいと思います。
あまりXAML触ったことなかったのですがコードとの連携がキモくて面白いですね。
参考
https://mseeeen.msen.jp/wpf-datagrid-checkbox-column-with-bulk-selector/
https://stackoverflow.com/questions/25643765/wpf-datagrid-databind-to-datatable-cell-in-celltemplates-datatemplate
https://stackoverflow.com/questions/21356662/selectall-in-datagrid
もしお役に立ちましたらスキ(♡マーク)をお願いします!!! noteアカウントがなくても大丈夫です。サポートもお待ちしています!