WPFでの入力値検証・その5 ~INotifyDataErrorInfo実装のベースクラスを作成~
(10/20追記)
この記事で書いたValidateableBaseクラスでは、複数のエラーが発生した時に正しくエラー状態を保持できません。
↓に続きを書きました。
WPFでの入力値検証・その6 ~複数のエラー表示への対応~ - SourceChord
立て続けにWPFでの入力検証ネタを続けましたが、
INotifyDataErrorInfoを実装するには結構なコード量が必要になります。
また、毎度同じようなコードの記述をすることになります。
ということで、INotifyDataErrorInfo実装のための、ベースクラスを作ってみました。
ValidateableBaseというクラスを作って、そっちにINotifyDataErrorInfoの実装をまとめています。
以下のクラス図のような継承関係です。
こうすることで、Validation機能付きのVMをスッキリと作成できます。
使い方
今回作成するValidateableBaseクラスを継承してViewModelをつくります。
で、入力値の検証を行いたいプロパティには、DataAnnotationsの属性を指定して、検証内容の設定を行います。
class MainWindowViewModel : ValidateableBase { private string inputString; [Required(ErrorMessage = "何か入力してください")] [StringLength(10, ErrorMessage = "10文字以内で入力してください")] public string InputString { get { return inputString; } set { this.SetProperty(ref this.inputString, value); } } }
<TextBox Width="120" Height="23" Margin="50" HorizontalAlignment="Left" VerticalAlignment="Top" Text="{Binding InputString, UpdateSourceTrigger=PropertyChanged}" Validation.ErrorTemplate="{StaticResource ValidationTemplate}" />
コード
今回のサンプルの全コードは以下の通りです。
BindableBase.cs
以前↓の記事で書いたものに少し手を加えました。
WPFでもBindableBaseを使ってINotifyPropertyChangedを実装する - SourceChord
SetPropertyメソッドにvirtualを追加して、継承先でオーバーライドできるようにしてます。
/// <summary> /// モデルを簡略化するための <see cref="INotifyPropertyChanged"/> の実装。 /// </summary> public abstract class BindableBase : INotifyPropertyChanged { /// <summary> /// プロパティの変更を通知するためのマルチキャスト イベント。 /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// プロパティが既に目的の値と一致しているかどうかを確認します。必要な場合のみ、 /// プロパティを設定し、リスナーに通知します。 /// </summary> /// <typeparam name="T">プロパティの型。</typeparam> /// <param name="storage">get アクセス操作子と set アクセス操作子両方を使用したプロパティへの参照。</param> /// <param name="value">プロパティに必要な値。</param> /// <param name="propertyName">リスナーに通知するために使用するプロパティの名前。 /// この値は省略可能で、 /// CallerMemberName をサポートするコンパイラから呼び出す場合に自動的に指定できます。</param> /// <returns>値が変更された場合は true、既存の値が目的の値に一致した場合は /// false です。</returns> protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null) { if (object.Equals(storage, value)) return false; storage = value; this.OnPropertyChanged(propertyName); return true; } /// <summary> /// プロパティ値が変更されたことをリスナーに通知します。 /// </summary> /// <param name="propertyName">リスナーに通知するために使用するプロパティの名前。 /// この値は省略可能で、 /// <see cref="CallerMemberNameAttribute"/> をサポートするコンパイラから呼び出す場合に自動的に指定できます。</param> protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { var eventHandler = this.PropertyChanged; if (eventHandler != null) { eventHandler(this, new PropertyChangedEventArgs(propertyName)); } } }
ValidateableBase.cs
今回作成した、入力値検証用のベースクラスです。
BindableBaseクラスの継承と、INotifyDataErrorInfoの実装を行います。
public abstract class ValidateableBase : BindableBase, INotifyDataErrorInfo { /// <summary> /// プロパティが既に目的の値と一致しているかどうかを確認します。必要な場合のみ、 /// プロパティを設定し、リスナーに通知します。 /// その後、プロパティの入力値検証を行います。 /// </summary> /// <typeparam name="T">プロパティの型。</typeparam> /// <param name="storage">get アクセス操作子と set アクセス操作子両方を使用したプロパティへの参照。</param> /// <param name="value">プロパティに必要な値。</param> /// <param name="propertyName">リスナーに通知するために使用するプロパティの名前。 /// この値は省略可能で、 /// CallerMemberName をサポートするコンパイラから呼び出す場合に自動的に指定できます。</param> /// <returns>値が変更された場合は true、既存の値が目的の値に一致した場合は /// false です。</returns> protected override bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null) { var isChanged = base.SetProperty(ref storage, value, propertyName); if (isChanged) this.ValidateProperty(value, propertyName); return isChanged; } protected void ValidateProperty(object value, [CallerMemberName]string propertyName = null) { var context = new ValidationContext(this) { MemberName = propertyName }; var validationErrors = new List<ValidationResult>(); if (!Validator.TryValidateProperty(value, context, validationErrors)) { var errors = validationErrors.Select(error => error.ErrorMessage); foreach (var error in errors) { AddError(propertyName, error); } } else { RemoveError(propertyName); } } #region 発生中のエラーを保持する処理を実装 readonly Dictionary<string, List<string>> _currentErrors = new Dictionary<string, List<string>>(); protected void AddError(string propertyName, string error) { if (!_currentErrors.ContainsKey(propertyName)) _currentErrors[propertyName] = new List<string>(); if (!_currentErrors[propertyName].Contains(error)) { _currentErrors[propertyName].Add(error); OnErrorsChanged(propertyName); } } protected void RemoveError(string propertyName) { if (_currentErrors.ContainsKey(propertyName)) _currentErrors.Remove(propertyName); OnErrorsChanged(propertyName); } #endregion private void OnErrorsChanged(string propertyName) { var h = this.ErrorsChanged; if (h != null) { h(this, new DataErrorsChangedEventArgs(propertyName)); } } #region INotifyDataErrorInfoの実装 public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; public System.Collections.IEnumerable GetErrors(string propertyName) { if (string.IsNullOrEmpty(propertyName) || !_currentErrors.ContainsKey(propertyName)) return null; return _currentErrors[propertyName]; } public bool HasErrors { get { return _currentErrors.Count > 0; } } #endregion }
MainWindowViewModel.cs
class MainWindowViewModel : ValidateableBase { private string inputString; [Required(ErrorMessage = "何か入力してください")] [StringLength(10, ErrorMessage = "10文字以内で入力してください")] public string InputString { get { return inputString; } set { this.SetProperty(ref this.inputString, value); } } public MainWindowViewModel() { this.InputString = string.Empty; } }
MainWindow.xaml
<Window x:Class="WpfBaseTemplate1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfBaseTemplate1" Title="MainWindow" Width="525" Height="350"> <Window.DataContext> <local:MainWindowViewModel /> </Window.DataContext> <Window.Resources> <ControlTemplate x:Key="ValidationTemplate"> <StackPanel> <TextBlock Foreground="Red" Text="{Binding AdornedElement.(Validation.Errors)[0].ErrorContent, ElementName=adornedelem}" /> <AdornedElementPlaceholder x:Name="adornedelem" /> </StackPanel> </ControlTemplate> </Window.Resources> <Grid> <TextBox Width="120" Height="23" Margin="50" HorizontalAlignment="Left" VerticalAlignment="Top" Text="{Binding InputString, UpdateSourceTrigger=PropertyChanged}" Validation.ErrorTemplate="{StaticResource ValidationTemplate}" /> </Grid> </Window>