概要
Windows環境において、
DataGridを使いたいからWPFを使う
みたいな意見は結構あるだろう。
しかし、DataGridの各列にイベントを付与するのは思ったより大変。
そこで、DataGridのシンプルな列機能DataGridTextColumnを段階的に改造することで、
より汎用的な特性を持つDataGridを作ってみよう、というのがこの記事である。
WPFに慣れるための、私自身の整理としても使いたい。
進化のさせ方
見た目はこんな感じのやつを進化させる
デフォルトの列
何にもないシンプルなデータ列。
当然、デフォルトのままなので、機能なんて存在しない。
コードビハインドのデータ列
コードビハインドで、以下の機能が追加されたデータ列。
- 入力中にEscapeボタンがおされたら、セルが入力終了されてしまうのを抑制する機能
- 入力中に数字以外のキーが押されたら、セルに値を入力しない機能。
コードビハインドと聞くと、MVVMをやっている人から見たら蕁麻疹が出るかもしれないが、
別に*.xaml.csに書くわけではないので、そこまで辛いコードにはならない。
状況次第では、この機能で十分良い場合も割と多い気がする。
Behaviorのデータ列
コードビハインドはごっつい気持ち悪いというときのために、
Behaviorを使って同じ機能を実装する。
- メリットは、Behaviorの使いまわしが出来る点。
- デメリットは、BehaviorとDataGridTextColumn用のクラス最低2種類を管理しないといけない点。
WPFをより使いこなしたい人にとっては重要項目であるが、
ここまでやるかは正直チーム次第なところもあるような気はしている。
UserStyleBehaviorのデータ列
Behaviorが専用だなんて、Behaviorの最大のメリットが生かせてないじゃないか。
BehaviorとDataGridTextColumnは分離されて実装すべきだ。
こういった声にこたえるのが、汎用的なBehaviorのデータ列である。
この例は、割と近年のWPFの世界ではメジャーな解決策になることが多い印象がある。
だが、何が問題かというと、とにかく元コードが難しい。
リフレクションを使ったり、DependencyPropertyなどを使ったり、
普通なら管理者がきちんとルールを作らないと、辛くなりそうな部分のコード。
私自身もこの世界ではベテランではないので、挫折するところだ。
これが必要な場合、パターン化して使っているが、
私は、これで解決する必要がない場合は、結構避けている印象あり。
参考サイト
実現方法
ライブラリ
- Prism.Wpfをnugetで入れてしまいましょう。今回は8.1.97で検証
- .NET 6を使ってみます
- 開発環境はVisual Studio 2022 Community
0.準備
DataGridTextColumnを継承したクラスを使用する
基本的には次のような構成でデータ列を使います
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using Microsoft.VisualBasic;
namespace CustomizedDataGrid001
{
public class BaseUserTextColumn : DataGridTextColumn
{
/// <summary>
/// 列が生成されるときに実行される
/// </summary>
/// <param name="cell">セル</param>
/// <param name="dataItem">Context</param>
/// <returns></returns>
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
return base.GenerateElement(cell, dataItem);
}
/// <summary>
/// 列が編集されるときに実行される
/// </summary>
/// <param name="cell">セル</param>
/// <param name="dataItem">Context</param>
/// <returns></returns>
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
var textbox = (TextBox)base.GenerateEditingElement(cell, dataItem);
return textbox;
}
}
}
ポイントとして、GenerateEditingElementでは、DataGridCellではなくTextBoxをあてがっている点である。
まあ、確かによく見ると、DataGridが文字列入力可能な時はTextBoxが表示されていることが分かるであろう。
DataGridTextColumnの編集時にはこのような挙動になることがポイントで、
他のコンポーネントを使用する際にはこの概念を応用すればよい。
※GenerateElementメソッドについてはこちら
Viewを作成する
<Window x:Class="CustomizedDataGrid001.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:CustomizedDataGrid001"
xmlns:bh="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<DataGrid ItemsSource="{Binding Items}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="デフォルトの列"
Binding="{Binding DefaultItem}">
</DataGridTextColumn>
<local:CodeBehindTextColumn Header="TextColumnコードビハインドの列"
Binding="{Binding CodeBehindItem}">
</local:CodeBehindTextColumn>
<local:BehaviorTextColumn Header="Behaviorの列"
Binding="{Binding BehaviorItem}">
</local:BehaviorTextColumn>
<local:UserStyleBehaviorTextColumn Header="UserStyleBehaviorの列"
Binding="{Binding UserStyleBehaviorItem}">
<!-- ☆ここにいろいろ書く -->
</local:UserStyleBehaviorTextColumn>
</DataGrid.Columns>
</DataGrid>
</Window>
- コードビハインドで使用するクラスをCodeBehindTextColumn
- 専用のBehaviorで使用するクラスをBehaviorTextColumn
- 汎用のBehaviorで使用するクラスをUserStyleBehaviorTextColumn
とする。
Behaviorを使うのに、肝心のBehaviorの記述がないじゃんと思った方、それは適切である。
その話については、UserStyleBehaviorTextColumnで、☆ここにいろいろ書くを埋めるところだ。
ViewModelを作成する
DataGridなので、ViewModelとしてはObservableCollectionクラスでバインドする。
とりあえずシンプルな構成で、
- MainViewModel
- BranchViewModel
という感じで。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using System.Windows.Input;
using Prism.Mvvm;
using Prism.Commands;
namespace CustomizedDataGrid001
{
public class MainViewModel : BindableBase
{
public ObservableCollection<BranchViewModel> Items
{
get;
set;
}
public MainViewModel()
{
Items = new ObservableCollection<BranchViewModel>()
{
new BranchViewModel()
{
DefaultItem = "デフォルト",
CodeBehindItem = "コードビハインド",
BehaviorItem = "ビヘイビア",
UserStyleBehaviorItem = "ビヘイビアインタラクション",
},
new BranchViewModel()
{
DefaultItem = "DEFAULT",
CodeBehindItem = "CODEBEHIND",
BehaviorItem = "BEHAVIOR",
UserStyleBehaviorItem = "BEHAVIOR INTERACTION",
}
};
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Prism.Mvvm;
using Prism.Commands;
namespace CustomizedDataGrid001
{
public class BranchViewModel : BindableBase
{
public string DefaultItem
{
get;
set;
}
public string CodeBehindItem
{
get;
set;
}
public string BehaviorItem
{
get;
set;
}
public string UserStyleBehaviorItem
{
get;
set;
}
}
}
1. コードビハインド
コードビハインドは、俗にいうxaml.csに書かれるいわゆるゴリゴリにイベントを書くコードである。
現在ではPrismを使う上ではあまりここを書くのは美しくないとされるものの、どうしても書かないといけないときもある。
とは言え今回書くコードは、xaml.csには書かない。
コードビハインドではあるものの、単に該当する処理が書かれないという感じで、
こちらの方が扱いの都合がいい場合もまあまああると思う。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using Microsoft.VisualBasic;
namespace CustomizedDataGrid001
{
public class CodeBehindTextColumn : DataGridTextColumn
{
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
cell.PreviewKeyDown -= Cell_PreviewKeyDown;
cell.PreviewKeyDown += Cell_PreviewKeyDown;
return base.GenerateElement(cell, dataItem);
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
cell.PreviewTextInput -= Cell_PreviewTextInput;
cell.PreviewTextInput += Cell_PreviewTextInput;
return base.GenerateEditingElement(cell, dataItem);
}
private void Cell_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if(e.Key == System.Windows.Input.Key.Escape)
{
e.Handled = true;
}
}
private void Cell_PreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
{
int val = 0;
if(!int.TryParse(e.Text, out val))
{
e.Handled = true;
}
}
}
}
やっていることは、とても単純だ。
- GenerateElementでセルを生成するときに、PreviewKeyDownイベントをデリゲートで指定。
- GenerateEditingElementを生成するときに、PreviewTextInputイベントをデリゲートで指定。
- デリゲート先に実行したい処理を書く(e.Handled = trueとすれば、入力が終わった扱いにできる)
2. 専用のビヘイビア
だが、コードビハインドを使うと、いくつかの問題もある。
それはイベントの汎用性が失われる。
ビヘイビアはコンポーネントに対して直接イベントを記述することが出来るので、イベント自体を汎用的に取り扱える。
数字以外入力禁止だったり、Escキー禁止だったり、いろいろ入れるわけだが、こういったパターンさえ持っておけば、1つのクラスで管理出来るのは強みであろう。
とは言っても、ビヘイビアの組み方は単純。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using Microsoft.Xaml.Behaviors;
namespace CustomizedDataGrid001
{
public class BehaviorTextColumn : DataGridTextColumn
{
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
Interaction.GetBehaviors(cell).Add(new KeyEscapeBehavior());
return base.GenerateElement(cell, dataItem);
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
var textbox = (TextBox)base.GenerateEditingElement(cell, dataItem);
Interaction.GetBehaviors(textbox).Add(new KeyOnlyNumericBehavior());
return textbox;
}
}
}
ここで新たに、KeyEscapeBehaviorクラスとKeyOnlyNumericBehaviorクラスが追加された。
まず、KeyEscapeBehaviorビヘイビアを次のように書いてしまおう。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using Microsoft.Xaml.Behaviors;
namespace CustomizedDataGrid001
{
public class KeyEscapeBehavior : Behavior<DataGridCell>
{
protected override void OnAttached()
{
AssociatedObject.PreviewKeyDown += AssociatedObject_PreviewKeyDown;
}
protected override void OnDetaching()
{
AssociatedObject.PreviewKeyDown -= AssociatedObject_PreviewKeyDown;
}
private void AssociatedObject_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Escape)
{
e.Handled = true;
}
}
}
}
Behaviorクラスはジェネリックが使われていて、割り当てたいコンポーネントをAssociatedObjectプロパティとして割り当てることが出来る。
ビヘイビアを直接割り当てるのがなぜ難しいのか
本来通常のコンポーネントであれば、次のようにxamlで書くことでビヘイビアを割り当てられるはず
<!-- 上省略 -->
<bh:Interaction.Behaviors>
<local:KeyEscapeBehavior />
</bh:Interaction.Behaviors>
<!-- 下省略 -->
ところが、これはこのDataGridTextColumnでは実現できない。
実現するには、CellStyleを使って
<local:BehaviorTextColumn.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="bh:Interaction.Behaviors"> <!-- 実行エラーが発生する -->
<Setter.Value>
<local:KeyEscapeBehavior />
</Setter.Value>
</Setter>
</Style>
</local:BehaviorTextColumn.CellStyle>
などとして、Interaction.Behaviorsに直接KeyEscapeBehaviorを割り当てていく必要があるのだが、これは案の定ArgumentNullExceptionエラーとなってしまう。
でも、それは当然なのだ。
Setter Propertyと書いてある項目は、BehaviorTextColumnのプロパティでなければいけないが、
そもそもInteractionというのは別のクラスであり、Behaviorsは実は単なる静的プロパティに過ぎないのだ
(詳しい技術情報は分からない部分も多いので、勉強中であるがそういうことだと思われる)。
専用のビヘイビアは、上記の問題を解決するための手段の一つであり、
ビヘイビアの動きを継承で組み込むことで、
ビヘイビアを使えるようにしている。
それが、GenerateElementの以下の部分
Interaction.GetBehaviors(cell).Add(new KeyEscapeBehavior());
そのものなのだ。
これを本来xamlで組めさえすればいいが、実用上はこれでも割と十分なことも多い。
なお、KeyOnlyNumericBehavior.csも大体こんな感じで書ける。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using Microsoft.Xaml.Behaviors;
namespace CustomizedDataGrid001
{
public class KeyOnlyNumericBehavior : Behavior<TextBox>
{
protected override void OnAttached()
{
AssociatedObject.PreviewTextInput += AssociatedObject_PreviewTextInput;
}
protected override void OnDetaching()
{
AssociatedObject.PreviewTextInput -= AssociatedObject_PreviewTextInput;
}
private void AssociatedObject_PreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
{
int val = 0;
if (!int.TryParse(e.Text, out val))
{
e.Handled = true;
}
}
}
}
3. 汎用的ビヘイビア
先ほどのコード
<local:BehaviorTextColumn.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="bh:Interaction.Behaviors"> <!-- 実行エラーが発生する -->
<Setter.Value>
<local:KeyEscapeBehavior />
</Setter.Value>
</Setter>
</Style>
</local:BehaviorTextColumn.CellStyle>
を解決するための手段が、汎用的なビヘイビアの活用である。
この実装をするためのコードUserStyleBehaviorCollection.csは以下の通り。
ここで、UserStyleBehaviorCollectionというのは、bh:Interaction.Behaviorsのラッパーの役割を果たす。
従って、bh:Interaction.Behaviorsの記述の代替をすることで、汎用的なビヘイビアを実装できるようになるのだ。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Xaml.Behaviors;
namespace CustomizedDataGrid001
{
public class UserStyleBehaviorCollection : FreezableCollection<Behavior>
{
public static readonly DependencyProperty GeneratedCellBehaviorsProperty =
DependencyProperty.RegisterAttached(
"GeneratedCellBehaviors",
typeof(UserStyleBehaviorCollection),
typeof(UserStyleBehaviorCollection),
new PropertyMetadata((sender, e) =>
{
if (e.OldValue == e.NewValue) { return; }
var value = e.NewValue as UserStyleBehaviorCollection;
if (value == null) { return; }
var behaviors = Interaction.GetBehaviors(sender);
behaviors.Clear();
foreach (var b in value.Select(x => (Behavior)x.Clone()))
{
behaviors.Add(b);
}
}));
public static readonly DependencyProperty GeneratedEditingCellBehaviorsProperty =
DependencyProperty.RegisterAttached(
"GeneratedEditingCellBehaviors",
typeof(UserStyleBehaviorCollection),
typeof(UserStyleBehaviorCollection),
new PropertyMetadata((sender, e) =>
{
if (e.OldValue == e.NewValue) { return; }
var value = e.NewValue as UserStyleBehaviorCollection;
if (value == null) { return; }
var behaviors = Interaction.GetBehaviors(sender);
behaviors.Clear();
foreach (var b in value.Select(x => (Behavior)x.Clone()))
{
behaviors.Add(b);
}
}));
public static UserStyleBehaviorCollection GetGeneratedCellBehaviors(DependencyObject obj)
{
return (UserStyleBehaviorCollection)obj.GetValue(GeneratedCellBehaviorsProperty);
}
public static UserStyleBehaviorCollection GetGeneratedEditingCellBehaviors(DependencyObject obj)
{
return (UserStyleBehaviorCollection)obj.GetValue(GeneratedEditingCellBehaviorsProperty);
}
public static void SetGeneratedCellBehaviors(DependencyObject obj, UserStyleBehaviorCollection value)
{
obj.SetValue(GeneratedCellBehaviorsProperty, value);
}
public static void SetGeneratedEditingCellBehaviors(DependencyObject obj, UserStyleBehaviorCollection value)
{
obj.SetValue(GeneratedEditingCellBehaviorsProperty, value);
}
protected override Freezable CreateInstanceCore()
{
return new UserStyleBehaviorCollection();
}
}
}
このやり方に関する詳しい解説は
に掲載されている。
ざっくりかいつまむと、
- GeneratedCellBehaviorsPropertyプロパティを、セルが生成されたときのビヘイビアの集合として定義する。xamlから見えるプロパティ名はGeneratedCellBehaviorsである。
- GeneratedEditingCellBehaviorsPropertyプロパティを、セルが編集開始のときのビヘイビアの集合として定義する。xamlから見えるプロパティ名はGeneratedEditingCellBehaviorsである。
このまま、そして、先ほどビヘイビアを直接割り当てるのがなぜ難しいのかでも触れたことだが、ここで定義したGeneratedCellBehaviorsやGeneratedEditingCellBehaviorsを直接Behaviorsに突っ込むことで汎用的に活用できるのだ。
また、もう一つやらないといけないこととして、GeneratedCellBehaviorsの方はCellStyleというのが標準でついているが、GeneratedEditingCellBehaviorsについては標準ではないので、新たにStyleクラスを追加する必要がある。
ここではそのプロパティをCellEditingStyleとして、GenerateEditingElementが実行されるときに
新たにStyleとして付与する。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using Microsoft.Xaml.Behaviors;
namespace CustomizedDataGrid001
{
public class UserStyleBehaviorTextColumn : DataGridTextColumn
{
public Style CellEditingStyle
{
get;
set;
}
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
return base.GenerateElement(cell, dataItem);
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
var textbox = (TextBox)base.GenerateEditingElement(cell, dataItem);
textbox.Style = CellEditingStyle;
return textbox;
}
}
}
これで準備は完了、
MainWindow.xamlを改めて書き直して
<Window x:Class="CustomizedDataGrid001.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:CustomizedDataGrid001"
xmlns:bh="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<DataGrid ItemsSource="{Binding Items}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="デフォルトの列"
Binding="{Binding DefaultItem}">
</DataGridTextColumn>
<local:CodeBehindTextColumn Header="TextColumnコードビハインドの列"
Binding="{Binding CodeBehindItem}">
</local:CodeBehindTextColumn>
<local:BehaviorTextColumn Header="Behaviorの列"
Binding="{Binding BehaviorItem}">
</local:BehaviorTextColumn>
<local:UserStyleBehaviorTextColumn Header="UserStyleBehaviorの列"
Binding="{Binding UserStyleBehaviorItem}">
<local:UserStyleBehaviorTextColumn.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="local:UserStyleBehaviorCollection.GeneratedCellBehaviors">
<Setter.Value>
<local:UserStyleBehaviorCollection>
<local:KeyEscapeBehavior />
</local:UserStyleBehaviorCollection>
</Setter.Value>
</Setter>
</Style>
</local:UserStyleBehaviorTextColumn.CellStyle>
<local:UserStyleBehaviorTextColumn.CellEditingStyle>
<Style TargetType="{x:Type TextBox}">
<Setter Property="local:UserStyleBehaviorCollection.GeneratedEditingCellBehaviors">
<Setter.Value>
<local:UserStyleBehaviorCollection>
<local:KeyOnlyNumericBehavior />
</local:UserStyleBehaviorCollection>
</Setter.Value>
</Setter>
</Style>
</local:UserStyleBehaviorTextColumn.CellEditingStyle>
</local:UserStyleBehaviorTextColumn>
</DataGrid.Columns>
</DataGrid>
</Window>
とすることで、ビヘイビアをxamlに紐づけることが出来る。
WPF公式のやり方に準じたやり方での実装が出来たことになる。
しかし・・・
正直言って、最後のやり方のコードはxamlファイルのコード量が馬鹿長くなるデメリットがある。
StaticResourceなどを活用して、別のコードに移すなり工夫しないと見づらいが、
その一方でそのルールが不明瞭の場合、あっちこっちに定義が飛んでしまう問題も発生する。
まとめ
- コードビハインドを使うやり方
- 専用のビヘイビアを使うやり方
- 汎用のビヘイビアをプロパティを新たに定義して使うやり方
をまとめた。
私的には、多くの場合には、1. 2.のやり方の方がやりやすいので、
大規模開発でもない限り、3.を使うことはない気がする。
WPFを使うと、どうしてもxamlが肥大化しやすく、そのxamlも見づらくなりやすい傾向にあるような気がするから。