Microsoft.TeamFoundation.MVVM.ViewModelBase + INotifyDataErrorInfo を使ってデータバインディングし、入力値の検証と表示をしてみる

書籍「ひと目でわかる Visual C# 2013/2012 アプリケーション開発入門」では、Microsoft.TeamFoundation.MVVM.ViewModelBaseを継承し、IDataErrorInfoインタフェースを実装したクラスをViewModelの基底クラス(ModelBase)としてデータバインディングしていました。

 
写経後、.NET Framework4.5よりWPFINotifyDataErrorInfoインタフェースが使えるとのことを知りました。
INotifyDataErrorInfo インターフェイス (System.ComponentModel)

 
そこで、

  • Microsoft.TeamFoundation.MVVM.ViewModelBaseを継承したクラスを作成
  • そのクラスでINotifyDataErrorInfoインタフェースを実装・データバインディング
  • 入力値の検証と表示

を試してみました。

 
ちなみに、Microsoft.TeamFoundation.MVVM.ViewModelBaseクラスはINotifyPropertyChangedインタフェースを実装しているので*1、INotifyPropertyChangedに関することを自分で実装しなくて済むのが良いです。

 
2015/12/10 追記 ここから

公式Blogに以下の記事が掲載されましたので、Microsoft.TeamFoundation.MVVMの使用前に記事を確認してみてください。
Microsoft.TeamFoundation.MVVM 名前空間の利用について - Visual Studio サポート チーム blog - Site Home - MSDN Blogs

2015/12/10 追記 ここまで

 

環境

なお、.NET Framework4.5は、WindowsVistaでもインストールできるので、Vistaでも使えそうです。
.NET Framework システム要件 - MSDN

 

作るもの

TextBoxを一つ用意し、スペースもNGとした入力必須のチェックを行います。

エラーとなった場合は、ToolTipにエラーを表示し、TextBoxの背景色も変更します。

起動時

f:id:thinkAmi:20140712060259p:plain

スペースを入力

f:id:thinkAmi:20140712060338p:plain

 

流れ

ViewModel用の継承元クラスの作成(VMBase.cs)
ViewModelBaseの継承とインタフェースを実装したクラスを用意

IDataErrorInfoと異なり、INotifyDataErrorInfoは一つのプロパティに対し複数のエラーを返すことができるので、書籍で使っていたフィールド_errorsの型をそれに合わせて変更しておきます。

また、書籍同様Microsoft.TeamFoundation.MVVM名前空間を使うため、Microsoft.TeamFoundation.Controlsを参照に追加しておきます。

class VMBase : ViewModelBase, INotifyDataErrorInfo
{
    private readonly Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
}

 

インタフェースの実装
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

public bool HasErrors
{
    get { return _errors.Count != 0; }
}

public System.Collections.IEnumerable GetErrors(string propertyName)
{
    if (string.IsNullOrWhiteSpace(propertyName) || !_errors.ContainsKey(propertyName))
    {
        return null;
    }

    return _errors[propertyName];
}

 
なお、書籍のModelBaseクラスにあるメソッドで不要と思われるものは削除しておきます。

メソッド 理由
Errorメソッド このサンプルでは使わないため
インデクサ(this[string propertyName]) GetErrorsメソッドと一致しているため

 

RaisePropertyChangedメソッドのオーバーライド

ViewModelBaseクラスのRaisePropertyChangedメソッドの場合、プロパティ名の文字列を引数として渡す必要がありました。

.NET Framework4.5からはCallerMemberName属性を使って呼び出し元のプロパティ名を取得できるため*2メソッドをオーバーライドしておきます。

protected override void RaisePropertyChanged([CallerMemberName]string propertyName = "")
{
    base.RaisePropertyChanged(propertyName);
}

 

エラー情報を更新するメソッドを用意

UpdateErrorsメソッド_errorsに含まれるエラー情報の追加や削除を行った上で、RaiseErrorsChangedメソッドでエラー情報の変更があったことを通知します。

protected void UpdateErrors([CallerMemberName]string propertyName = "", string errorMessage = "")
{
    if (string.IsNullOrWhiteSpace(errorMessage))
    {
        _errors.Remove(propertyName);
    }
    else
    {
        if (!_errors.ContainsKey(propertyName))
        {
            _errors[propertyName] = new HashSet<string>();
        }

        _errors[propertyName].Add(errorMessage);
    }

    RaiseErrorsChanged(propertyName);
}

public void RaiseErrorsChanged(string propertyName)
{
    if (ErrorsChanged != null)
    {
        ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }
}

 

ViewModelクラスの作成(TextBoxViewModel.cs)

上記で作成したクラスVMBaseを継承して、データバインドに必要なプロパティCommentを実装します。

なお、検証が終わった時に、エラー情報の更新を通知するUpdateErrorsメソッドと、プロパティの値が変更されたことを通知するRaisePropertyChangedメソッドを、それぞれ忘れないように呼びます。

class TextBoxViewModel : VMBase
{
    private string _comment;
    public string Comment
    {
        get { return _comment; }
        set
        {
            if (_comment == value) return;

            _comment = value;

            if (string.IsNullOrWhiteSpace(value))
            {
                UpdateErrors(errorMessage: "入力必須です");
            }
            else
            {
                UpdateErrors();
            }

            RaisePropertyChanged();
        }
    }
}

 

Viewの作成(TextBox.xaml)
データバインドの設定を記載

DataContextを設定するために、Window要素にViewModelが属する名前空間を追加します。

<Window ...
    xmlns:local="clr-namespace:MVVMApp"
    ...>

 
対象のデータバインド対象のクラスをDataContextに記載します。

<Window.DataContext>
    <local:TextBoxViewModel />
</Window.DataContext>

 

データバインド対象のTextBoxを記載

BindingのPathに、DataContextで設定したクラスのプロパティCommentを設定します。

また、TextBoxに文字を入力するたびにエラーチェックをするよう、バインディングソースの更新トリガーとして、UpdateSourceTriggerを設定します。

なお、INotifyDataErrorInfoの通知を受け取る設定のValidatesOnNotifyDataErrorsはデフォルトでtrueのため、設定は省略します。
Binding.ValidatesOnNotifyDataErrors プロパティ (System.Windows.Data) - MSDN

<StackPanel>
    <TextBox Margin="30" Text="{Binding Path=Comment, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>

 

エラー時のToolTip設定を記載
Styleを適用する対象の設定

TargetTypeにTextBoxを指定します。

<Style TargetType="TextBox">

 

Styleを適用するトリガーの設定

Validation.HasError添付プロパティが変更された時を指定します。

<Trigger Property="Validation.HasError" Value="True">

 

エラーを表示するToolTipの設定

ToolTipが表示される位置として、TextBoxを指定します。

また、ToolTipの表示内容として、Validation.Errors添付プロパティのErrorContentプロパティからエラー情報を取得したものを指定します。
Validation.Errors アタッチされるプロパティ (System.Windows.Controls) - MSDN

なお、Validation.Errorsは添付プロパティなので、Pathで使うには()で囲む必要があります*3
Binding.Path プロパティ (System.Windows.Data) - MSDN

<Setter Property="ToolTip" Value="{Binding Path=(Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource Self}}" />

 

エラー時のTextBoxの表示を記載

TextBoxの背景色もPinkに変更します。

<Setter Property="Background" Value="Pink" />

 

Style全体

ToolTipとTextBoxに関するStyleの全体は以下の通りとなります。

<Window.Resources>
    <Style TargetType="TextBox">
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="True">
                <Setter Property="ToolTip" 
                        Value="{Binding Path=(Validation.Errors)[0].ErrorContent,
                                        RelativeSource={RelativeSource Self}}" />
                <Setter Property="Background" Value="Pink" />
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>

 

 
以上で、Microsoft.TeamFoundation.MVVM + INotifyDataErrorInfoを使った実装ができました。

 

ソースコード

GitHubに上げておきました。
CSharp-Sample/MVVMApp at master · thinkAmi/CSharp-Sample

 

更新履歴
2014/7/15

UpdateErrorsメソッドで、List<string>型でエラーメッセージの重複チェックをしていたが、HashSet<string>型を使えば重複チェックが不要になる上、パフォーマンス的にも良さそうなので、後者へと変更
参考: HashSet vs List vs Dictionary | theburningmonk.com

 

参考

*1:実際にはその親クラスのNotifyPropertyChangedDispatcherObjectで実装しています

*2:MSDNでも、便利ですとの記載があります - CallerMemberNameAttribute クラス - System.Runtime.CompilerServices - MSDN

*3:こちらにはカッコで囲む理由の記載があります - プロパティ パス構文 (Windows)- MSDN