web-dev-qa-db-ja.com

MVVMによる適切な検証

警告:非常に長く詳細な投稿

さて、MVVMを使用する場合のWPFでの検証。私は今、多くのことを読んで、多くのSOの質問を見て、many)アプローチを試みましたが、すべてはある時点でややハックを感じます'正しい方法™をどうやってやるかわからない。

理想的には、 IDataErrorInfo ;を使用してビューモデルですべての検証が発生するようにします。それが私がしたことです。ただし、このソリューションを検証トピック全体の完全なソリューションにしないさまざまな側面があります。

状況

次の簡単なフォームを見てみましょう。ご覧のとおり、空想ではありません。ビューモデルのstringおよびintプロパティにバインドする2つのテキストボックスがあります。さらに、ICommandにバインドされたボタンがあります。

Simple form with only a string and integer input

そのため、検証には次の2つの選択肢があります。

  1. テキストボックスの値が変更されるたびに、検証を自動的に実行できます。そのため、ユーザーは無効なものを入力するとすぐに応答します。
    • エラーが発生したときにボタンを無効にするために、これをさらに一歩進めることができます。
  2. または、ボタンが押されたときにのみ検証を明示的に実行し、該当する場合はすべてのエラーを表示できます。ここでエラーのボタンを無効にすることはできません。

理想的には、選択肢1を実装します。通常のデータバインディングで ValidatesOnDataErrors が有効になっている場合、これはデフォルトの動作です。したがって、テキストが変更されると、バインディングはソースを更新し、そのプロパティのIDataErrorInfo検証をトリガーします。エラーはビューに報告されます。ここまでは順調ですね。

ビューモデルの検証ステータス

興味深い点は、ビューモデル、またはこの場合はボタンにエラーがあるかどうかを知らせることです。 IDataErrorInfoの動作方法は、主にビューにエラーを報告するためにあります。そのため、ビューはエラーがあるかどうかを簡単に確認し、エラーを表示し、さらに Validation.Errors を使用して注釈を表示できます。さらに、検証は常に単一のプロパティを参照して行われます。

そのため、エラーがある場合、または検証が成功した場合にビューモデルに知らせるのは難しいです。一般的な解決策は、ビューモデル自体のすべてのプロパティに対してIDataErrorInfo検証をトリガーすることです。これは、多くの場合、個別のIsValidプロパティを使用して行われます。利点は、コマンドを無効にするためにも簡単に使用できることです。欠点は、これによりすべてのプロパティの検証が少し頻繁に実行される可能性があることですが、ほとんどの検証はパフォーマンスを損なわない程度に十分であるべきです。別の解決策は、検証を使用してどのプロパティがエラーを生成したかを覚えて、それらのみをチェックすることですが、ほとんどの場合、それは少し複雑で不要なようです。

一番下の行は、これがうまく機能する可能性があるということです。 IDataErrorInfoはすべてのプロパティの検証を提供します。ビューモデル自体でそのインターフェイスを使用して、オブジェクト全体に対しても検証を実行できます。問題の紹介:

例外のバインド

ビューモデルは、プロパティに実際の型を使用します。したがって、この例では、整数プロパティは実際のintです。ただし、ビューで使用されるテキストボックスは、ネイティブでtextのみをサポートします。そのため、ビューモデルでintにバインドするとき、データバインディングエンジンは自動的に型変換を実行します(または少なくとも試行します)。数字用のテキストボックスにテキストを入力できる場合、内部に常に有効な数字が存在しない可能性が高くなります。そのため、データバインディングエンジンは変換に失敗し、FormatExceptionをスローします。

Data binding engine throws an exception and that’s displayed in the view

ビュー側では、簡単にそれを見ることができます。バインディングエンジンからの例外はWPFによって自動的にキャッチされ、エラーとして表示されます。セッターでスローされる例外に必要な Binding.ValidatesOnExceptions を有効にする必要さえありません。ただし、エラーメッセージには一般的なテキストが含まれているため、問題になる可能性があります。 Binding.UpdateSourceExceptionFilter ハンドラーを使用して、スローされている例外を調べ、ソースプロパティを確認し、代わりに一般的ではないエラーメッセージを生成することで、これを自分で解決しました。これらはすべて独自のBindingマークアップ拡張機能にカプセル化されているため、必要なすべてのデフォルトを使用できます。

だから、ビューは素晴らしいです。ユーザーはエラーを作成し、エラーフィードバックを確認して修正できます。ただし、ビューモデルは失われます。バインディングエンジンが例外をスローしたため、ソースは更新されませんでした。したがって、ビューモデルは古い値のままであり、これはユーザーに表示されるものではなく、IDataErrorInfo検証は明らかに適用されません。

さらに悪いことに、ビューモデルがこれを知る良い方法はありません。少なくとも、これに対する良い解決策はまだ見つかっていません。可能性のあることは、エラーが発生したことをビューモデルにビューレポートに戻すことです。これは Validation.HasError プロパティをビューモデルにデータバインドすることで実行できます(直接は不可能です)。そのため、ビューモデルは最初にビューの状態を確認できます。

別のオプションは、Binding.UpdateSourceExceptionFilterで処理される例外をビューモデルにリレーすることです。そのため、同様に通知されます。ビューモデルは、これらのことを報告するバインディングのインターフェイスを提供することもでき、タイプごとの一般的なエラーメッセージではなく、カスタムエラーメッセージを許可します。しかし、それにより、ビューからビューモデルへのより強い結合が作成されるため、通常は避けたいと思います。

別の「解決策」は、すべての型付きプロパティを取り除き、プレーンなstringプロパティを使用し、代わりにビューモデルで変換を行うことです。これは明らかにすべての検証をビューモデルに移動しますが、データバインディングエンジンが通常処理するものの信じられないほどの重複も意味します。さらに、ビューモデルのセマンティクスを変更します。私にとって、ビューはビューモデル用に構築されており、その逆ではありません。もちろん、ビューモデルの設計は、ビューが何をするかによって異なりますが、ビューがそれをどのように行うかは一般的な自由です。したがって、ビューモデルは、番号があるためintプロパティを定義します。ビューでは、テキストボックスを使用して(これらすべての問題を許可する)、または数値でネイティブに機能するものを使用できます。いいえ、プロパティのタイプをstringに変更することは私にとってオプションではありません。

最後に、これはビューの問題です。ビュー(およびそのデータバインディングエンジン)は、ビューモデルに適切な値を与える役割を果たします。ただし、この場合、ビューモデルに古いプロパティ値を無効にする必要があることを伝える良い方法はないようです。

BindingGroups

バインディンググループ は、これに取り組むための1つの方法です。バインディンググループには、IDataErrorInfoやスローされた例外など、すべての検証をグループ化する機能があります。ビューモデルで使用できる場合、それらは、たとえば CommitEdit を使用して、それらの検証ソースのallの検証ステータスをチェックする手段も備えています。

デフォルトでは、バインディンググループは上記の選択肢2を実装します。バインディングを明示的に更新し、本質的に追加のuncommitted状態を追加します。したがって、ボタンをクリックすると、コマンドはそれらの変更をcommitし、ソースの更新とすべての検証をトリガーし、成功した場合は単一の結果を取得できます。したがって、コマンドのアクションは次のようになります。

 if (bindingGroup.CommitEdit())
     SaveEverything();

CommitEditは、all検証が成功した場合にのみtrueを返します。 IDataErrorInfoを考慮し、バインディング例外もチェックします。これは選択2の完璧な解決策のようです。少し面倒なのはバインディングでバインディンググループを管理することだけですが、私はほとんどこれを処理するものを自分で構築しました( related )。

バインディングにバインディンググループが存在する場合、バインディングはデフォルトで明示的な UpdateSourceTrigger になります。バインディンググループを使用して上記の選択肢1を実装するには、基本的にトリガーを変更する必要があります。とにかくカスタムバインディング拡張機能があるので、これはかなり単純で、すべての変数をLostFocusに設定するだけです。

そのため、テキストフィールドが変更されるたびにバインディングが更新されます。ソースを更新できる場合(バインディングエンジンは例外をスローしません)、IDataErrorInfoは通常どおり実行されます。更新できなかった場合、ビューは引き続き表示できます。ボタンをクリックすると、基になるコマンドはCommitEditを呼び出し(コミットする必要はありませんが)、検証結果全体を取得して続行できるかどうかを確認できます。

この方法では、ボタンを簡単に無効にできない場合があります。少なくともビューモデルからは。検証を何度もチェックすることは、コマンドステータスを更新するだけでは実際には良い考えではなく、ビューエンジンは、バインディングエンジン例外がスローされたとき(それからボタンを無効にする必要があります)に通知されません。ボタンを再度有効にします。 Validation.HasError を使用して、ビュー内のボタンを無効にするトリガーを追加することもできるため、不可能ではありません。

溶液?

全体として、これは完璧なソリューションのようです。しかし、それに関する私の問題は何ですか?正直に言うと、私は完全にはわかりません。バインディンググループは、通常は小さなグループで使用されると思われる複雑なものであり、単一のビューに複数のバインディンググループがある場合があります。検証を確実にするためだけに、ビュー全体に1つの大きなバインディンググループを使用することにより、悪用しているように感じます。そして、私はこれらの問題を抱えている唯一の人ではないので、この状況全体を解決するためのより良い方法が必要だと考え続けています。これまでのところ、MVVMでの検証にバインディンググループを使用している人はほとんどいませんでした。

それでは、バインディングエンジンの例外を確認しながら、MVVMを使用してWPFで検証を行う適切な方法は何ですか?


私のソリューション(/ hack)

まず、ご意見ありがとうございます!上記で書いたように、私はすでにIDataErrorInfoを使用してデータの検証を行っており、検証ジョブを実行するのに最も快適なユーティリティであると個人的に考えています。以下の回答でシェリダンが提案したものと同様のユーティリティを使用しているので、うまく機能します。

結局、私の問題はバインド例外の問題に要約されました。ビューモデルはそれがいつ発生したかを知らないだけでした。上記のようにバインディンググループを使用してこれを処理することはできましたが、私はまだそれに満足していなかったので、私はまだそれに反対することを決めました。それで、私は代わりに何をしましたか?

前述したように、バインディングのUpdateSourceExceptionFilterをリ​​ッスンすることにより、ビュー側でバインディング例外を検出します。そこでは、バインディング式の DataItem からビューモデルへの参照を取得できます。次に、ビューモデルをバインディングエラーに関する情報の可能な受信者として登録するインターフェイスIReceivesBindingErrorInformationがあります。次に、それを使用して、バインディングパスと例外をビューモデルに渡します。

object OnUpdateSourceExceptionFilter(object bindExpression, Exception exception)
{
    BindingExpression expr = (bindExpression as BindingExpression);
    if (expr.DataItem is IReceivesBindingErrorInformation)
    {
        ((IReceivesBindingErrorInformation)expr.DataItem).ReceiveBindingErrorInformation(expr.ParentBinding.Path.Path, exception);
    }

    // check for FormatException and produce a nicer error
    // ...
 }

ビューモデルでは、パスのバインディング式について通知されるたびに記憶します。

HashSet<string> bindingErrors = new HashSet<string>();
void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception)
{
    bindingErrors.Add(path);
}

そして、IDataErrorInfoがプロパティを再検証するたびに、バインディングが機能したことを知っており、ハッシュセットからプロパティをクリアできます。

ビューモデルでは、ハッシュセットにアイテムが含まれているかどうかを確認し、データを完全に検証する必要があるアクションを中止できます。ビューからビューモデルへの結合のため、これは最適なソリューションではないかもしれませんが、そのインターフェイスを使用することで、少なくとも多少問題は少なくなります。

56
poke

警告:長い回答も(

検証にはIDataErrorInfoインターフェイスを使用しますが、自分のニーズに合わせてカスタマイズしています。あなたもそれがあなたの問題のいくつかを解決することがわかると思います。質問との違いの1つは、基本データ型クラスに実装することです。

あなたが指摘したように、このインターフェースは一度に1つのプロパティを処理するだけですが、明らかにこの時代と時代では、それは良くありません。そこで、代わりに使用するコレクションプロパティを追加しました。

protected ObservableCollection<string> errors = new ObservableCollection<string>();

public virtual ObservableCollection<string> Errors
{
    get { return errors; }
}

外部エラーを表示できないという問題に対処するために(あなたの場合はビューからですが、私の場合はビューモデルから)、単に別のコレクションプロパティを追加しました。

protected ObservableCollection<string> externalErrors = new ObservableCollection<string>();

public ObservableCollection<string> ExternalErrors
{
    get { return externalErrors; }
}

私のコレクションを見るHasErrorプロパティがあります:

public virtual bool HasError
{
    get { return Errors != null && Errors.Count > 0; }
}

これにより、カスタムBoolToVisibilityConverterを使用して、これをGrid.Visibilityにバインドできます。内部にコレクションコントロールがあるGridを表示し、エラーがある場合にエラーを表示します。また、BrushRedに変更してエラーを強調表示することもできます(別のConverterを使用)が、アイデアは得られると思います。

次に、各データ型またはモデルクラスで、Errorsプロパティをオーバーライドし、Itemインデクサーを実装します(この例では簡略化しています)。

public override ObservableCollection<string> Errors
{
    get
    {
        errors = new ObservableCollection<string>();
        errors.AddUniqueIfNotEmpty(this["Name"]);
        errors.AddUniqueIfNotEmpty(this["EmailAddresses"]);
        errors.AddUniqueIfNotEmpty(this["SomeOtherProperty"]);
        errors.AddRange(ExternalErrors);
        return errors;
    }
}

public override string this[string propertyName]
{
    get
    {
        string error = string.Empty;
        if (propertyName == "Name" && Name.IsNullOrEmpty()) error = "You must enter the Name field.";
        else if (propertyName == "EmailAddresses" && EmailAddresses.Count == 0) error = "You must enter at least one e-mail address into the Email address(es) field.";
        else if (propertyName == "SomeOtherProperty" && SomeOtherProperty.IsNullOrEmpty()) error = "You must enter the SomeOtherProperty field.";
        return error;
    }
}

AddUniqueIfNotEmptyメソッドはカスタムextensionメソッドであり、「ブリキに書かれていることを行います」。重複するエラーを無視して、検証する各プロパティを順番に呼び出し、それらからコレクションをコンパイルする方法に注意してください。

ExternalErrorsコレクションを使用して、データクラスで検証できないものを検証できます。

private void ValidateUniqueName(Genre genre)
{
    string errorMessage = "The genre name must be unique";
    if (!IsGenreNameUnique(genre))
    {
        if (!genre.ExternalErrors.Contains(errorMessage)) genre.ExternalErrors.Add(errorMessage);
    }
    else genre.ExternalErrors.Remove(errorMessage);
}

ユーザーがintフィールドにアルファベット文字を入力する状況に関するあなたのポイントに対処するために、TextBoxにカスタムIsNumeric AttachedPropertyを使用する傾向があります。私は彼らにこの種のエラーをさせません。私はいつも、それを起こさせてから修正するよりも、それを止めた方が良いと感じています。

全体として、WPFでの検証機能に本当に満足しており、まったく望んでいません。

最後に、完全を期すために、この追加機能の一部を含むINotifyDataErrorInfoインターフェースが存在するという事実に注意する必要があると感じました。詳細については、MSDNの INotifyDataErrorInfo Interface ページをご覧ください。


更新>>>

はい、ExternalErrorsプロパティを使用して、そのオブジェクトの外部からデータオブジェクトに関連するエラーを追加します。申し訳ありませんが、私の例は完了していません... IsGenreNameUniqueメソッドでは、LinQデータ項目のallGenreを使用することがわかります。オブジェクトの名前が一意かどうかを判断するコレクション:

private bool IsGenreNameUnique(Genre genre)
{
    return Genres.Where(d => d.Name != string.Empty && d.Name == genre.Name).Count() == 1;
}

int/string問題については、データクラスでthoseエラーが発生するのを確認できる唯一の方法です。すべてのプロパティをobjectとして宣言しているのに、それから非常に多くのキャストを行う必要がある場合です。おそらく、次のようにプロパティを2倍にすることができます。

public object FooObject { get; set; } // Implement INotifyPropertyChanged

public int Foo
{
    get { return FooObject.GetType() == typeof(int) ? int.Parse(FooObject) : -1; }
}

次に、Fooがコードで使用され、FooObjectBindingで使用された場合、これを行うことができます。

public override string this[string propertyName]
{
    get
    {
        string error = string.Empty;
        if (propertyName == "FooObject" && FooObject.GetType() != typeof(int)) 
            error = "Please enter a whole number for the Foo field.";
        ...
        return error;
    }
}

そうすれば要件を満たすことができますが、追加するコードがたくさんあります。

17
Sheridan

大量の追加コードを実装したくない場合に、物事を単純化するための努力があります...

シナリオは、ビューモデルにintプロパティ(10進数または別の非文字列型)があり、ビューでテキストボックスをそれにバインドすることです。

プロパティのセッターで起動するビューモデルに検証があります。

ビューでは、ユーザーは123abcと入力し、ビューロジックはビュー内のエラーを強調表示しますが、値が間違ったタイプであるため、プロパティを設定できません。セッターは呼び出されません。

最も簡単な解決策は、ビューモデルのintプロパティを文字列プロパティに変更し、モデルに値を出し入れすることです。これにより、不良テキストがプロパティのセッターにヒットし、検証コードでデータをチェックして、必要に応じて拒否できます。

WPFのIMHO検証は壊れています。これは、以前に与えられた問題を人々が回避しようとした精巧な(そして巧妙な)方法からわかるようにです。私にとっては、大量の余分なコードを追加したり、テキストボックスを検証できるように独自の型クラスを実装したりしたくないので、これらのプロパティを文字列に基づいて生きることができます汚い。

Microsoftはこれを修正して、intまたはdecimalプロパティにバインドされたテキストボックスでの無効なユーザー入力のシナリオが、この事実をビューモデルにエレガントに伝えることができるようにする必要があります。たとえば、XAMLコントロールの新しいバインドプロパティを作成して、ビューロジック検証エラーをビューモデル内のプロパティに伝えることができるはずです。

このトピックに対する詳細な回答を提供してくれた他の人に感謝と敬意を表します。

1
Richard Moore

欠点は、これによりすべてのプロパティの検証が少し頻繁に実行される可能性があることですが、ほとんどの検証はパフォーマンスを損なわない程度に十分であるべきです。別の解決策は、検証を使用してどのプロパティがエラーを生成したかを覚えてチェックすることですが、ほとんどの場合、それは少し複雑で不要なようです。

どのプロパティにエラーがあるかを追跡する必要はありません。エラーが存在することを知るだけで済みます。ビューモデルはエラーのリストを保持でき(エラーの概要を表示するのにも役立ちます)、IsValidプロパティはリストに何かがあるかどうかを単純に反映できます。エラーの概要が最新であり、IsValidが変更されるたびに更新されることを確認する限り、IsValidが呼び出されるたびにすべてをチェックする必要はありません。


最後に、これはビューの問題です。ビュー(およびそのデータバインディングエンジン)は、ビューモデルに適切な値を与える役割を果たします。ただし、この場合、ビューモデルに古いプロパティ値を無効にする必要があることを伝える良い方法はないようです。

ビューモデルにバインドされているコンテナ内のエラーをリッスンできます。

container.AddHandler(Validation.ErrorEvent, Container_Error);

...

void Container_Error(object sender, ValidationErrorEventArgs e) {
    ...
}

これにより、エラーが追加または削除されたときに通知され、e.Error.Exceptionが存在するため、ビューはバインディング例外のリストを保持し、ビューモデルに通知できます。

ただし、ビューはその役割を適切に満たしていないため、この問題の解決策は常にハックになります。これにより、ビューモデル構造を読み取り、更新する手段がユーザーに与えられます。これは、ユーザーにtextボックスではなく、何らかの種類の "integer box"を正しく提示するまでの一時的な解決策と見なす必要があります。

1
nmclean

私の意見では、問題は検証が多すぎる場所で発生していることにあります。また、すべての検証ログインをViewModelで書きたいと思っていましたが、それらのすべての番号のバインドによってViewModelがおかしくなりました。

失敗しないバインディングを作成して、この問題を解決しました。明らかに、バインディングが常に成功する場合、型自体がエラー状態を適切に処理する必要があります。

失敗可能な値のタイプ

失敗した変換を適切にサポートするジェネリック型を作成することから始めました。

public struct Failable<T>
{
    public T Value { get; private set; }
    public string Text { get; private set; }
    public bool IsValid { get; private set; }

    public Failable(T value)
    {
        Value = value;

        try
        {
            var converter = TypeDescriptor.GetConverter(typeof(T));
            Text = converter.ConvertToString(value);
            IsValid = true;
        }
        catch
        {
            Text = String.Empty;
            IsValid = false;
        }
    }

    public Failable(string text)
    {
        Text = text;

        try
        {
            var converter = TypeDescriptor.GetConverter(typeof(T));
            Value = (T)converter.ConvertFromString(text);
            IsValid = true;
        }
        catch
        {
            Value = default(T);
            IsValid = false;
        }
    }
}

無効な入力文字列(2番目のコンストラクター)のために型の初期化に失敗しても、invalid textとともに無効な状態を静かに保存することに注意してください。これは、バインディングのラウンドトリップをサポートするために必要です間違った入力の場合でも。

汎用値コンバーター

上記のタイプを使用して、汎用値コンバーターを作成できます。

public class StringToFailableConverter<T> : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value.GetType() != typeof(Failable<T>))
            throw new InvalidOperationException("Invalid value type.");

        if (targetType != typeof(string))
            throw new InvalidOperationException("Invalid target type.");

        var rawValue = (Failable<T>)value;
        return rawValue.Text;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value.GetType() != typeof(string))
            throw new InvalidOperationException("Invalid value type.");

        if (targetType != typeof(Failable<T>))
            throw new InvalidOperationException("Invalid target type.");

        return new Failable<T>(value as string);
    }
}

XAML Handy Converters

ジェネリックのインスタンスを作成して使用するのはXAMLでの苦痛なので、一般的なコンバーターの静的インスタンスを作成しましょう。

public static class Failable
{
    public static StringToFailableConverter<Int32> Int32Converter { get; private set; }
    public static StringToFailableConverter<double> DoubleConverter { get; private set; }

    static Failable()
    {
        Int32Converter = new StringToFailableConverter<Int32>();
        DoubleConverter = new StringToFailableConverter<Double>();
    }
}

他の値タイプは簡単に拡張できます。

使用法

使い方はとても簡単です。タイプをintからFailable<int>に変更するだけです:

ViewModel

public Failable<int> NumberValue
{
    //Custom logic along with validation
    //using IsValid property
}

XAML

<TextBox Text="{Binding NumberValue,Converter={x:Static local:Failable.Int32Converter}}"/>

このように、IDataErrorInfoプロパティをチェックすることにより、INotifyDataErrorInfoで同じ検証メカニズム(ViewModelまたはIsValidまたはその他のもの)を使用できます。 IsValidがtrueの場合、Valueを直接使用できます。

1
Hemant

OK、あなたが探していた答えを見つけたと思います...
説明するのは簡単ではありません-しかし..
一度説明すると非常にわかりやすい...
「標準」または少なくとも試行された標準として見られるMVVMに対して最も正確/「認定」されていると思います。

しかし、始める前に.. MVVMに関して慣れている概念を変更する必要があります。

「さらに、ビューモデルのセマンティクスが変更されます。私にとって、ビューはビューモデル用に構築されており、その逆ではありません。ビューがそれを行う自由」

その段落が問題の原因です。-なぜですか?

あなたが述べているのは、View-Modelがビューに合わせて調整する役割を持たないためです。
それは多くの点で間違っています-非常に簡単に証明しますが..

次のようなプロパティがある場合:

public Visibility MyPresenter { get...

ビューに役立つものでない場合、Visibilityとは何ですか?
プロパティに指定されるタイプ自体と名前は、ビュー用に明確に構成されます。

私の経験によると、MVVMには2つの区別可能なView-Modelsカテゴリがあります。

  • プレゼンタービューモデル-ボタン、メニュー、タブ項目などにフックします...
  • エンティティビューモデル-エンティティデータを画面に表示するコントロールに連結されます。

これらは2つの異なるもので、まったく異なる懸念事項です。

そして今、ソリューションに:

public abstract class ViewModelBase : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;

   public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
   {
      if (PropertyChanged != null)
         PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
   }
}


public class VmSomeEntity : ViewModelBase, INotifyDataErrorInfo
{
    //This one is part of INotifyDataErrorInfo interface which I will not use,
    //perhaps in more complicated scenarios it could be used to let some other VM know validation changed.
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; 

    //will hold the errors found in validation.
    public Dictionary<string, string> ValidationErrors = new Dictionary<string, string>();

    //the actual value - notice it is 'int' and not 'string'..
    private int storageCapacityInBytes;

    //this is just to keep things sane - otherwise the view will not be able to send whatever the user throw at it.
    //we want to consume what the user throw at us and validate it - right? :)
    private string storageCapacityInBytesWrapper;

    //This is a property to be served by the View.. important to understand the tactic used inside!
    public string StorageCapacityInBytes
    {
       get { return storageCapacityInBytesWrapper ?? storageCapacityInBytes.ToString(); }
       set
       {
          int result;
          var isValid = int.TryParse(value, out result);
          if (isValid)
          {
             storageCapacityInBytes = result;
             storageCapacityInBytesWrapper = null;
             RaisePropertyChanged();
          }
          else
             storageCapacityInBytesWrapper = value;         

          HandleValidationError(isValid, "StorageCapacityInBytes", "Not a number.");
       }
    }

    //Manager for the dictionary
    private void HandleValidationError(bool isValid, string propertyName, string validationErrorDescription)
    {
        if (!string.IsNullOrEmpty(propertyName))
        {
            if (isValid)
            {
                if (ValidationErrors.ContainsKey(propertyName))
                    ValidationErrors.Remove(propertyName);
            }
            else
            {
                if (!ValidationErrors.ContainsKey(propertyName))
                    ValidationErrors.Add(propertyName, validationErrorDescription);
                else
                    ValidationErrors[propertyName] = validationErrorDescription;
            }
        }
    }

    // this is another part of the interface - will be called automatically
    public IEnumerable GetErrors(string propertyName)
    {
        return ValidationErrors.ContainsKey(propertyName)
            ? ValidationErrors[propertyName]
            : null;
    }

    // same here, another part of the interface - will be called automatically
    public bool HasErrors
    {
        get
        {
            return ValidationErrors.Count > 0;
        }
    }
}

そして今、あなたのコードのどこか-あなたのボタンコマンド「CanExecute」メソッドは、その実装にVmEntity.HasErrorsへの呼び出しを追加できます。

そして、これからの検証に関するあなたのコードに平和がありますように:)

0
G.Y