CSVのクラスマッピングの定義をC#スクリプトで記述する (その2:Converter)
この記事は、以下の記事の続きです。
- この記事のサンプルコードはこちら
目次
前回は、CSVデータのクラスマッピングをC#スクリプトで記述してC#のオブジェクトに格納するところまでやりました。
そのオブジェクトをWPFのDataGridコントロールにバインドして、読み取ったデータが表示されることも確認できました。
ただ、このままではDataGridのセルに表示されるデータが元のフォーマットと異なって表示されてしまう場合があります。
DataGridの表示フォーマットを何とかしたい
今回は、以下のCSVデータを使用します。
名前,生年月日,配偶者,性別,お小遣い 佐藤,1986/9/2,なし,男性,\1000 鈴木,2001/8/10,なし,男性,\500 高橋,1974/6/7,あり,女性,\300 田中,1990/11/20,なし,男性,\1000 伊藤,1986/10/10,なし,男性,\2000 渡辺,1968/5/5,なし,女性,\800 山本,1970/9/3,あり,女性,\9000 中村,1984/3/15,なし,男性,\10000 小林,2005/1/2,なし,男性,\100 加藤,1955/6/10,あり,男性,\0 吉田,1986/9/12,あり,男性,\800 山田,1996/3/25,なし,女性,\500 佐々木,1986/8/3,なし,男性,\500 山口,1986/12/6,あり,女性,\6000 松本,1980/2/2,あり,男性,\8000 井上,1992/7/17,あり,女性,\9000 木村,1989/11/6,なし,男性,\10000 林,1999/10/2,あり,男性,\200 斎藤,2010/4/21,なし,女性,\1000 清水,1980/9/7,あり,男性,\600
このデータを読み込んで生成したオブジェクトをそのまま表示すると以下のようになります。
期待する表示内容とは異なる表示になっている箇所がいくつかあります。
- カラムヘッダがデータ格納クラスのプロパティ名になっている
⇒ 読み込んだCSVファイルのヘッダ名を表示したい - 「生年月日」に時刻が含まれています。年月日の並びもおかしい
⇒yyyy/M/d
のフォーマットで表示したい - 「お小遣い」の
¥(\)
マークが表示されない
⇒¥10,000
の様に表示させたい
CsvClassMap
で指定した文字列のフォーマットや変換処理をDataGridの表示に流用する
CSV読み書き時に使用するCsvClassMap<T>
はC#スクリプトで以下のように記述しました。
RegisterClassMap<Person>(classMap => { classMap.Map(m => m.Name).Name("名前"); classMap.Map(m => m.Birthday).Name("生年月日") .TypeConverterOption("yyyy/M/d"); classMap.Map(m => m.Married).Name("配偶者") .TypeConverterOption(true, "あり") .TypeConverterOption(false, "なし"); classMap.Map(m => m.Sex).Name("性別"); classMap.Map(m => m.PocketMoney).Name("お小遣い") .TypeConverterOption(NumberStyles.Currency) .TypeConverterOption("C"); });
DataGridに表示される形式は、CSVファイルに保存される形式と合わせたいのですが、 DataGridの表示フォーマットのために上記のような記述を2度も書くのはいやですよね。
ここはどうにかして、CsvClassMap の設定値を使いまわす方法を考えたいと思います。
CsvClassMap
を覗いてみる
CsvClassMap のメンバーを確認してみます。
カラムごとの設定は PropertyMaps
というコレクションに入っているPropertyMapクラスのDataプロパティに格納されているようです。
var propertyMap = csvClassMap.PropertyMaps .FirstOrDefault(m.Data.Property.Name == propertyName); var propertyMapData = propertyMap.Data;
このプロパティはCsvPropertyMapData
というクラスで、以下のようなメンバーを持ちます(抜粋)。
//Property(カラム)毎のマッピング設定を格納するクラス public class CsvPropertyMapData { //プロパティ情報 public virtual PropertyInfo Property { get; } //カラムヘッダ名(コレクション) public virtual CsvPropertyNameCollection Names { get; } //ヘッダ名のインデックス public virtual int NameIndex { get; set; } //カラムのインデックス public virtual int Index { get; set; } //テキスト⇔オブジェクト コンバータ public virtual ITypeConverter TypeConverter { get; set; } //コンバータでの変換時に使用するオプション public virtual TypeConverterOptions TypeConverterOptions { get; } //変換できなかった場合の規定値 public virtual object Default { get; set; } } //テキスト⇔オブジェクト間の変換で使用するオプション public class TypeConverterOptions { public CultureInfo CultureInfo { get; set; } public DateTimeStyles? DateTimeStyle { get; set; } public TimeSpanStyles? TimeSpanStyle { get; set; } public NumberStyles? NumberStyle { get; set; } public List<string> BooleanTrueValues { get; } public List<string> BooleanFalseValues { get; } public string Format { get; set; } }
- カラムヘッダには
Names
とNameIndex
が使えそうです。 - データの変換処理は、
TypeConverter
で行います。これは(特に指定しなければ)対応するデータの型に応じたコンバータが自動で割り当てられます。- CsvHelperで実装されているコンバータは以下を参照ください。CsvHelper/src/CsvHelper/TypeConversion at master · JoshClose/CsvHelper · GitHub
- 文字列のフォーマットやカルチャの設定等は
TypeConverterOptions
に格納されます。これはClassMap 登録時に.TypeConverterOption()
メソッドで設定します。
CsvClassMapのTypeConverter
(CsvHelper)を IValueConverter
(WPF) にラップしてDataGridColumn に設定する。
DataGridのカラムごとの変換処理は、 DataGridColumn のBindingプロパティに指定するコンバータを置き換えることで変更可能です。
コンバータはIValueConverter
を継承したクラスである必要があります。
以下に作成したコンバータを示します。中身は、CsvClassMapの TypeConverter をそのまま呼び出すだけのシンプルなものです。
public class DataGridColumnConverter : IValueConverter { private ITypeConverter typeConverter; private TypeConverterOptions converterOptions; public string HeaderName { get; } public DataGridColumnConverter(string headerName, ITypeConverter converter, TypeConverterOptions options) { typeConverter = converter ?? new DefaultTypeConverter(); converterOptions = options ?? new TypeConverterOptions(); HeaderName = headerName; } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (targetType != typeof(string)) { return value; } try { return typeConverter.ConvertToString(converterOptions, value); } catch { //変換に失敗した場合はDependencyProperty.UnsetValue を返す return DependencyProperty.UnsetValue; } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { var oldValue = value as string; if (oldValue == null) { return value; } try { return typeConverter.ConvertFromString(converterOptions, oldValue); } catch { //変換に失敗した場合はDependencyProperty.UnsetValue を返す return DependencyProperty.UnsetValue; } } }
コンバータ作成部分の処理は以下のようになります。ClassMapから propertyName に対応する PropertyMap
を取得して、そのTypeConverter
およびTypeConverterOptions
を用いてコンバータを生成します。
public class MainWindowViewModel : BindableBase { public DataGridColumnConverter GetDataGridColumnConverter(string propertyName) { var propertyMap = host.ClassMap.PropertyMaps .FirstOrDefault(m => m.Data.Property.Name == propertyName); if (propertyMap == null) { return null; } return new DataGridColumnConverter( propertyMap.Data.Names[propertyMap.Data.NameIndex], propertyMap.Data.TypeConverter, propertyMap.Data.TypeConverterOptions, propertyMap.Data.Default); } }
自動生成されるDataGridColumn をカスタマイズする
DataGrid のカラムは、バインドするデータに合わせて自動生成されます。
自動生成されるカラムはAutoGeneratingColumn
イベント内でカスタマイズすることができます。
<Grid> <DataGrid Grid.Column="0" AutoGenerateColumns="True" AutoGeneratingColumn="DataGrid_AutoGeneratingColumn" ItemsSource="{Binding Items}" > </DataGrid> </Grid>
AutoGeneratingColumn
イベントは、DataGrid.ItemsSourceプロパティが変更されて、個々のカラムが自動生成される際に発行されます。
カラムのカスタマイズ処理は、コードビハインドに追加した DataGrid_AutoGeneratingColumn
イベントハンドラ内に記述します。
イベント引数 DataGridAutoGeneratingColumnEventArgs e
のプロパティを用いて次のように設定します。
(e.Column を強引にキャストしてBindingを取得するなど、少々お行儀が悪いですが。。。)
- e.PropertyName から 対応するコンバータを取得
- e.Column.Header に 表示するカラム名を設定
- e.Column から Binding オブジェクトを取得して、コンバータを設定する
- e.Column が DataGridTextColumn だったら EditingElementStyle にXAML側で記述した変換エラー時のStyleを設定
public partial class MainWindow : Window { public MainWindow() { InitializeComponent() DataContext = new MainWindowViewModel(); } //AutoGeneratingColumn イベントハンドラ private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e) { var vm = DataContext as MainWindowViewModel; if (vm == null) { return; } //コンバータを取得 var converter = vm.GetDataGridColumnConverter(e.PropertyName); if (converter == null) { return; } if (!string.IsNullOrEmpty(converter.HeaderName)) { //カラムヘッダの文字列を変更 e.Column.Header = converter.HeaderName; } var textColumn = e.Column as DataGridTextColumn; if (textColumn != null) { //変換エラー時にToolチップを表示するスタイルを設定 textColumn.EditingElementStyle = (Style) Resources["textColumnStyle"]; } //コンバータを設定 var binding = (e.Column as DataGridBoundColumn)?.Binding as Binding; if (binding != null) { binding.Converter = converter; } } }
<!-- セル編集で変換エラー発生時のスタイル --> <Window.Resources> <Style x:Key="textColumnStyle" TargetType="{x:Type TextBox}"> <Style.Triggers> <Trigger Property="Validation.HasError" Value="True"> <!-- 背景を赤くする --> <Setter Property="Background" Value="Red"/> <!-- ツールチップを表示する --> <Setter Property="ToolTip"> <Setter.Value> <Binding Path="(Validation.Errors)[0].ErrorContent" RelativeSource="{x:Static RelativeSource.Self}"/> </Setter.Value> </Setter> </Trigger> </Style.Triggers> </Style> </Window.Resources>
実行結果
ひとまずこれで準備が整いました。では、実行してみましょう。
- [Read]を押して読み込んだ結果
- ちゃんと元データのカラム名が表示されています。
- 「生年月日」が
yyyy/M/d
フォーマットで表示されています。 - 「お小遣い」に
¥
マークがついています。(int.ToString("C")
のフォーマット)
いい感じです。 では、変換できない文字を入力した場合はどうなるか見てみましょう。
ちゃんと XAMLで書いたStyle通りにセルの背景が赤くなり、エラーメッセージがツールチップに表示されました!
まとめ
CsvHelperのTypeConverter
をラップした IValueConverter
を使う事でDataGridの表示形式を元のCSVファイルと合わせることが出来ました。
あとは、カラム毎に「数値の入力可能範囲」や、「入力可能な文字」等の細かなルールを指定できると良いですよね。
という事で、次回はその辺りの実装を考えてみたいと思います。