読者です 読者をやめる 読者になる 読者になる

開発記その 2 - バリデーション -

さてさて、 Silverlight では自前で頑張ったり、バインディング時に例外飛ばしたり、IDataErrorInfo を使って通知したりとかバリデーションとその表示方法がいくつかありますが、個人的には INotifyDataErrorInfo がいいかなと思ってます。何故かって言うと・・・

  • いつでも好きなタイミングでバリデーションエラーを通知できる
  • 同じ項目に複数のメッセージ(バリデーションエラー)を設定できる

とこですかね。
その他のバインディングを前提にしたのは、バインディング時しかエラーを出せないですからね。

ってことで、以下のような実装を。

private Dictionary<string, List<string>> errors = new Dictionary<string, List<string>>();

private string userName;
[Display(Name = "UserName", ResourceType = typeof(Assets.Resources.Strings))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(Assets.Resources.Strings))]
public string UserName {
  get { return this.userName; }
  set {
    if (this.userName != value) {
      this.userName = value;
      OnPropertyChanged("UserName");
      ValidateProperty("UserName", this.userName);
    }
  }
}

protected void ValidateProperty(string propertyName, object newValue) {
  List<ValidationResult> results = new List<ValidationResult>();
  Validator.TryValidateProperty(newValue, new ValidationContext(this, null, null) { MemberName = propertyName }, results);

  bool isChanged = false;

  if (results.Count == 0) {
    if (this.errors.ContainsKey(propertyName)) {
      this.errors.Remove(propertyName);
      isChanged = true;
    }
  } else {
    if (!this.errors.ContainsKey(propertyName)) {
      this.errors.Add(propertyName, new List<string>());
    }

    List<string> currentErrors = this.errors[propertyName];

    foreach (ValidationResult result in results) {
      if (!currentErrors.Contains(result.ErrorMessage)) {
        currentErrors.Add(result.ErrorMessage);
        isChanged = true;
      }
    }

    foreach (string currentError in currentErrors.ToArray()) {
      if (results.FirstOrDefault(result => result.ErrorMessage == currentError) == null) {
        currentErrors.Remove(currentError);
        isChanged = true;
      }
    }
  }

  if (isChanged) {
    OnErrorChanged(propertyName);
  }
}

で、実装するときに考えたのが、バリデーションをかけるタイミングです。上のコードではプロパティに値を設定しきって、変更通知も出したあとにバリデーションをかけてます。パッと見は気持ち悪いんですけどね。

なぜ、こうしたかってのは、値を設定し切る前にバリデーションかけてダメだったら値を設定しないようにすると画面上の見た目(テキストボックスとかの値)は変更されているのに、VM の値は変更されてない状態になって差異がでるからです。そんな差異が出てる状態で例えばボタンが押されたときにもう一度バリデーションをかけても、画面上とは違う値を利用する形になってしまうんです。タイミングによってはエラーになる値が入力されている可能性があるのにバリデーションが通ってしまうおそれもありますね。

で、この値を設定してからバリデーションをかけるようにすると、ある程度は大丈夫になるんですが、型変換エラーの場合はまだダメなんですね。例えば、int32 型で年齢とかのプロパティの場合、テキストボックスに "あいうえお" とか入力されると、値がそもそもプロパティにセットされない。で、画面上と VM の値に差異が出てしまう。

対処方として、プロパティは全部 String にしてしまうとかもあるんですが、それはそれでイケテない。ってことで、型変換エラーがでる入力ができないコントロールを作るのがベターですかね。このへんはまた作ったときに書こうと思います。

あと、プロパティに対応しないバリデーションとかもどうしようか悩み中。いま、 ValidationSummary を使ってるんですがこいつでそういうのって標準じゃ対応できないっぽいのです。まぁ、こっちも必要になったときに考えてまた書こうと思います。


※今回のチェックイン
http://moneybook.codeplex.com/SourceControl/changeset/changes/51612