書籍「ひと目でわかる Visual C# 2013/2012 アプリケーション開発入門」では、Microsoft.TeamFoundation.MVVM.ViewModelBase
を継承し、IDataErrorInfo
インタフェースを実装したクラスをViewModelの基底クラス(ModelBase)としてデータバインディングしていました。
写経後、.NET Framework4.5よりWPFでINotifyDataErrorInfo
インタフェースが使えるとのことを知りました。
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 追記 ここまで
環境
- Windows7 x64
- .NET Framework 4.5
- Microsoft.TeamFoundation.MVVM
- INotifyDataErrorInfo
なお、.NET Framework4.5は、WindowsVistaでもインストールできるので、Vistaでも使えそうです。
.NET Framework システム要件 - MSDN
作るもの
TextBoxを一つ用意し、スペースもNGとした入力必須のチェックを行います。
エラーとなった場合は、ToolTipにエラーを表示し、TextBoxの背景色も変更します。
起動時
スペースを入力
流れ
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
参考
- Validating Data in WPF 4.5 Using the INotifyErrorDataError Interface - TechNet Articles - United States (English) - TechNet Wiki
- Asynchronous validation in WPF using the MVVM pattern and INotifyDataErrorInfo - TechNet Articles - United States (English) - TechNet Wiki
- プロパティ変更とエラー情報の通知 (実装編) | Do Design Space
- 入力データ検証 その3 HasError添付プロパティ - Yuya Yamaki’s blog
- WPFでの入力値検証・その2 ~INotifyDataErrorInfoを使ってみる~ - SourceChord
*1:実際にはその親クラスのNotifyPropertyChangedDispatcherObjectで実装しています
*2:MSDNでも、便利ですとの記載があります - CallerMemberNameAttribute クラス - System.Runtime.CompilerServices - MSDN
*3:こちらにはカッコで囲む理由の記載があります - プロパティ パス構文 (Windows)- MSDN