CSVのクラスマッピングの定義をC#スクリプトで記述する (その3: ValidationRule)

この記事は、以下の記事の続きです

github.com

目次

入力可能なデータの条件を指定できるようにする

前回は、CsvHelperのテキスト変換処理(TypeConverter)ラップしたConverterを設定することで、DataGridColumn の変換処理をカスタマイズしました。
今回は、DataGridColumnの検証(Validation)機能を利用して、カラム毎に入力値の制限を付けられるようにしたいと思います。

C#スクリプト側の記述

スクリプト側に公開するインターフェースに以下のメソッドを追加して、カラム(データ格納クラスのプロパティ)毎に入力制限を設定できるようにします。

public interface ICsvEditorConfigurationHost
{
    void AddValidation<TType,TMember>(
        Expression<Func<TType, TMember>> memberSelector,
        Func<TMember, bool> validation,
        string errorMessage);
}
  • 第1引数: 式木(デリゲート)でデータ格納クラスの対象プロパティを指定
  • 第2引数:「対象プロパティの値を受け取りboolを返すデリゲート」で、入力可能な値の条件を指定
  • 第3引数: 入力制限に引っかかった場合のエラーメッセージを指定

スクリプト内での記述例は以下の通り。(前回の記事と同じサンプルデータを使用)

//生年月日は今日以前のみ
AddValidation<Person, DateTime>(prop => prop.Birthday,
    m => m <= DateTime.Today,
    "未来の日付は入力できません");

//お小遣いは ¥0 以上 ¥10,000 以下
AddValidation<Person, int>(prop => prop.PocketMoney,
    m => (m >= 0) && (m <= 10000),
    "入力可能な範囲は ¥0~¥10,000 です。");

AddValidation メソッドの実装は以下の通りです。 引数に渡した設定値をプロパティ名をキーとした辞書に登録します。

public class CsvEditorConfigurationHost
{
    public IDictionary<string, ColumnValidation> ColumnValidations { get; } 
        = new Dictionary<string, ColumnValidation>();

    public void AddValidation<TType, TMember>(Expression<Func<TType, TMember>> memberSelector, 
        Func<TMember, bool> validation, string errorMessage)
    {
        //式木からプロパティ名を取得
        MemberExpression memberExpression = null;
        if (memberSelector.Body.NodeType == ExpressionType.Convert)
        {
            var body = (UnaryExpression)memberSelector.Body;
            memberExpression = body.Operand as MemberExpression;
        }
        else if (memberSelector.Body.NodeType == ExpressionType.MemberAccess)
        {
            memberExpression = memberSelector.Body as MemberExpression;
        }
        if (memberExpression == null)
        {
            throw new ArgumentException("Not a member access", nameof(memberSelector));
        }
        //プロパティ名をキーにして辞書に登録
        ColumnValidations.Add(memberExpression.Member.Name, 
            new ColumnValidation(m => validation((TMember)m), errorMessage));
    }
}
//設定値格納用
public class ColumnValidation
{
    public Func<object, bool> Validation { get; }
    public string ErrorMessage { get; }
    public ColumnValidation(Func<object, bool> validation, string errorMessage)
    {
        Validation = validation;
        ErrorMessage = errorMessage;
    }
}

DataGridColumn に ValidationRuleを設定する

スクリプトで指定された条件からDataGridColumnバインドするValidationRule クラスを作成します。

public class DataGridColumnValidationRule : ValidationRule
{
    private Func<object, bool> isValidate;
    private object errorContent;

    public DataGridColumnValidationRule(Func<object, bool> isValidate, object errorContent)
    {
        if (isValidate == null) { throw new ArgumentNullException(nameof(isValidate)); }
        if (errorContent == null) { throw new ArgumentNullException(nameof(errorContent)); }

        ValidationStep = ValidationStep.ConvertedProposedValue;
        this.isValidate = isValidate;
        this.errorContent = errorContent ?? "invalid value";
    }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        return (isValidate(value)) ?
            ValidationResult.ValidResult :
            new ValidationResult(false, errorContent);
    }
}

前回のConverterの指定と同様にAutoGeneratingColumnイベントのハンドラ内でDataGridColumnのBindingオブジェクトにValidationRuleを追加します。

public class MainWindow
{
    private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
    {
        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)
        {
            textColumn.EditingElementStyle = (Style)Resources["textColumnStyle"];
        }

        var binding = (e.Column as DataGridBoundColumn)?.Binding as Binding;
        if (binding != null)
        {
            binding.Converter = converter;
            //VMからValidationRuleを取得してBindingに設定        
            var validationRule = VM.GetDataGridColumnValidation(e.PropertyName);
            if (validationRule != null)
            {
                binding.ValidationRules.Add(validationRule);
            }
        }
    }
}

public class MainWindowViewModel
{
    public DataGridColumnValidationRule GetDataGridColumnValidation(string propertyName)
    {
        ColumnValidation columnValidaiton;
        if (host.ColumnValidations.TryGetValue(propertyName, out columnValidaiton))
        {
            return new DataGridColumnValidationRule(columnValidaiton.Validation, columnValidaiton.ErrorMessage);
        }
        return null;
    }
}

実行例

「生年月日」と「お小遣い」に入力可能範囲外の値を入力してみます。

f:id:pierre3:20160722230823p:plain

期待通りセルの背景が赤くなり、ツールチップには指定したエラーメッセージが表示されています。

まとめ

これで入力ミスも柔軟にチェックすることが出来るようになりました。