WPFのDataGridに対してDataTableをバインドする際、ヘッダに特定の文字(/.[]() やアンダースコア '_')が含まれていると正しく表示が行われません。
今回は、これを回避するための方法と、それを簡単に実装するためのメソッドを紹介します。
同様の現象に悩まれている方は、コピペしていただければ解決できますので、是非お試しください。
発生する現象
現象は大きく分けて2つあります。1つはアンダースコアが含まれている場合、もう1つは /.[]() が含まれている場合です。
それぞれ現象が異なりますので、それぞれに対して解説致します。
カラム名のアンダースコアは表示されない
DataTableのカラム名にアンダースコア '_' が含まれている場合、DataGridのHeaderにアンダースコアが表示されません。
カラム名に /.[]() が含まれる列は値が空白になる
DataTableのカラム名(ColumnName)に /.[]() が含まれている場合、DataGridにバインドした瞬間、その列は値が表示されなくなります。
また、DataTableのカラム名に []() が含まれていて、[] 又は () が対になっていない場合、DataGridにバインドした瞬間にエラー(例外)が発生します。
例えば "住所[]" なら列の値が表示されないだけですが、"住所[" とか "住所[[1]" とかだとエラーになります。
アンダースコアや /.[]() が誤動作する理由
DataGridの仕様では、アンダースコアはアクセスキーとして、/.[]() は特別な意味を持つ予約文字として認識されます。
DataGrid にDataTableをバインドする際、DataGridのPropertyName にDataTableのカラム名(ColumnName)が自動でセットされるのですが、この時にアクセスキーや予約語として解釈してしまうがために、今回の様な現象が発生します。
従って、DataGridとしては正しい動作をしているだけで、逆に言うと我々はDataGridの想定外の動作を要求していると解釈できます。
アンダースコアや /.[]() を正しく表示する方法
アンダースコアの回避策
アンダースコアを表示させるには、アンダースコアをエスケープする方法と、アクセスキーとしてアンダースコアを使用させないよう RecognizesAccessKeyをFalse設定する方法の2通りがあります。
アンダースコアをエスケープするにはアンダースコアを2個連ねるだけです。例えばReplace メソッドで次の通り置換してやればOKです。
1 |
column.Header = column.Header.Replace("_","__"); |
もう1つは、XAML上のスタイル指定でアクセスキーを使用させないようにする方法です。こちらはXAML上で指定することになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<DataGrid> <DataGrid.ColumnHeaderStyle> <Style TargetType="{x:Type DataGridColumnHeader}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type DataGridColumnHeader}"> <Border> <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" RecognizesAccessKey="False"/> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </DataGrid.ColumnHeaderStyle> </DataGrid> |
/.[]() の回避策
DataTableを DataGrid にバインドする際、PropertyName にアクセスキーや予約語が含まれなければ問題は発生しません。
そこで、バインドのタイミングでは問題が発生しない一時的なカラム名を使い、画面表示のタイミングで正しいカラム名に戻すという方法を使います。
具体的には、DataTableのカラム名を Caption プロパティに退避し、代わりにカラム名に連番を設定します。
こうすることで、PropertyNameには連番の文字列がセットされるため、値が表示されなくなったり、エラーになることが回避できます。
次に、バインド時のカラム自動生成時に発生する AutoGeneratingColumn イベントの中で、Captionに退避していた元のカラム名を、DataGridのHeaderにセットします。
自作関数のソースコード
自作関数は次の3つを用意しています。
- AutoGeneratingColumn のイベントハンドラを設定する EnableHeaderFromCaptionme()
- カラム名(DataColumn)をCaptionに退避する StoreColumnNameToCaption()
- Captionからカラム名を復元する RestoreColumnNameToCaption()
EnableHeaderFromCaptionme()の引数はDataGridのインスタンス、StoreColumnNameToCaption()とRestoreColumnNameToCaption() はDataTableのインスタンスを渡します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
/// <summary> /// カラム生成時、Captionをヘッダに代入 /// </summary> /// <param name="dataGrid"></param> private void EnableHeaderFromCaption(DataGrid dataGrid) { dataGrid.AutoGeneratingColumn += (s, e) => { var column = (DataGridBoundColumn)e.Column; var dv = (DataView)((DataGrid)s).ItemsSource; var index = dv.Table.Columns.IndexOf(e.PropertyName); column.Header = dv.Table.Columns[index].Caption.Replace("_","__"); }; } /// <summary> /// カラム名をCaptionに退避し、ColumnNameに連番のカラム名をセット /// </summary> /// <param name="dt"></param> /// <returns></returns> private DataTable StoreColumnNameToCaption(DataTable dt) { Enumerable.Range(0, dt.Columns.Count).Select(i => { dt.Columns[i].Caption = dt.Columns[i].ColumnName; dt.Columns[i].ColumnName = "Columns" + i.ToString(); return i; }).ToArray(); return dt; } /// <summary> /// カラム名をCaptionからColumNameに復元 /// </summary> /// <param name="dt"></param> /// <returns></returns> private DataTable RestoreColumnNameToCaption(DataTable dt) { Enumerable.Range(0, dt.Columns.Count).Select(i => { dt.Columns[i].ColumnName = dt.Columns[i].Caption; return i; }).ToArray(); return dt; } |
この関数では、Captionから元のカラム名を取り出した後、アンダースコアをエスケープ(アンダースコアを2個連ねる)してHeaderに代入しています。
つまり、DataTableのカラム名(ColumnName)と、DataGridのカラム名(Header)はこの時点で異なることになります。表示するだけなら全く問題ありませんが、カラム名に対して検索を行う場合、アンダースコアが2個に置換されていることを意識する必要があります。
もしカラム名に対して検索を行うのであれば、元のDataTableのカラム名又はCaptionに対して検索するのが一番簡単ですが、DataGridのHeaderに対して検索を行う場合は、2個連なったアンダースコアを1つに置換した値を使う様にして下さい。
あるいは、アンダースコアの対策を XAML上(RecognizesAccessKey="False")で行うのであれば、EnableHeaderFromCaptionメソッドの12行目 column.Header = ~から .Replace("_","__")を削除することでHederに対する検索も可能となります。
具体的には、EnableHeaderFromCaptionメソッドを下記に置き換えてください。
1 2 3 4 5 6 7 8 9 10 |
private void EnableHeaderFromCaption(DataGrid dataGrid) { dataGrid.AutoGeneratingColumn += (s, e) => { var column = (DataGridBoundColumn)e.Column; var dv = (DataView)((DataGrid)s).ItemsSource; var index = dv.Table.Columns.IndexOf(e.PropertyName); column.Header = dv.Table.Columns[index].Caption; }; } |
自作関数の使い方
以下の解説は、DataGridのインスタンスに "uxDataGrid" という名前を付けていることを前提にしています。
まず最初に、EnableHeaderFromCaptionme() は、コンストラクタや Loaded イベントハンドラに記述します。
1 2 3 4 5 |
public DataGridControl() { InitializeComponent(); EnableHeaderFromCaption(uxDataGrid); } |
次に、DataGridのDataContextに対してDataTableを代入する際、StoreColumnNameToCaption() を呼んでください。
1 |
uxDataGrid.DataContext = StoreColumnNameToCaption(datatable); |
画面に表示するだけならこれでOKですが、例えば画面で編集したDataTableをCSVなどに保存する場合、本来のカラム名が出力されません。
そこで、DataTableのカラム名を本来のカラム名に戻さなければなりません。
この時にRestoreColumnNameToCaption() を使って、DataTableのカラム名を復元します。
1 |
RestoreColumnNameToCaption(datatable); |
この時点で、DataTableのカラム名には _/.[]() が含まれてしまうので、再び連番の文字列にしておかないと、表示がおかしくなってしまいます。
そこで、StoreColumnNameToCaption() を使ってDataTableのカラム名をCaptionに退避するとともに、カラム名を連番の文字列にしてしまいます。
1 |
StoreColumnNameToCaption(datatable) |
例えば、 CsvUtil().Write() というCSV出力用のメソッドを呼びだすことを例にすると次のようになります。
1 2 3 4 5 6 |
// Captionに退避していたカラム名をDataTableに反映 RestoreColumnNameToCaption(DataSource); // CSV出力の実行 new CsvUtil().Write(DataSource, filename, true); // DataTableのカラム名をCaptionに退避し、カラム名を連番化する StoreColumnNameToCaption(DataSource); |
まとめ
今回はDataGridにDataTableをバインドし、カラム名を自動生成させた場合、予約語がカラム名に含まれていると不具合(値が表示されない、エラーになる、アンダースコアが消える)が起きるので、その回避方法と、回避方法が簡単にできる関数を紹介しました。
カラム名が固定の場合はあまり関係ありませんが、任意のCSVを読み込んでDataGridに表示する場合、今回の不具合に遭遇する可能性があるので、その場合はこの回避方法をご検討ください。
コメント