WPFでの入力値検証・その6 ~複数のエラー表示への対応~
以前↓で書いたINotifyDataErrorInfoのベースクラスの実装ですが、一つのプロパティに複数のエラーが同時発生している、という状態を正しく表現できていなかったので修正しました。
WPFでの入力値検証・その5 ~INotifyDataErrorInfo実装のベースクラスを作成~ - SourceChord
複数のエラーが同時に発生するようにVMを修正
前回の実装では、複数のエラーが同時に発生しないので、以下のようにDataAnnotationsのエラー属性を増やしました。
RegularExpression属性で、小文字の英字のみを受け付けるという条件を追加しています。
private string inputString; [Required(ErrorMessage = "何か入力してください")] [StringLength(10, ErrorMessage = "10文字以内で入力してください")] [RegularExpression("[a-z]+", ErrorMessage = "a-zの文字列を入力してください。")] public string InputString { get { return inputString; } set { this.SetProperty(ref this.inputString, value); } }
複数エラーの表示に対応した、ErrorTemplate
複数のエラーが同時発生しているときに、すべてのエラー状態を表示できるようにErrorTemplateを修正しました。
以下のような感じ。
<ControlTemplate x:Key="ValidationTemplate"> <StackPanel> <ItemsControl ItemsSource="{Binding AdornedElement.(Validation.Errors), ElementName=adornedelem}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Foreground="Red" Text="{Binding ErrorContent}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <AdornedElementPlaceholder x:Name="adornedelem" /> </StackPanel> </ControlTemplate> <!--中略--> <TextBox Width="120" Height="23" Margin="50" HorizontalAlignment="Left" VerticalAlignment="Top" Text="{Binding InputString, UpdateSourceTrigger=PropertyChanged}" Validation.ErrorTemplate="{StaticResource ValidationTemplate}">
Validation.ErrorTemplateに設定するControlTemplate内で、ItemsControlを使い複数のエラーの内容を表示できるようにしています。
複数のエラーが発生した時には、以下のようにエラー表示が上に積み上がっていきます。
INotifyDataErrorInfoの実装を修正
前回の実装のバグ
前回作ったValidateableBaseクラスの実装では、複数のエラーが発生した後に、一部のエラーだけ解除されても、エラー状態が適切に解除されていませんでした。
例)
「何か入力してください」というエラーが発生しているときに、テキストブロックに数字を入力し、「a-zの文字列を入力してください。」というエラーを発生させると、
文字が入力されているのに、最初のエラーが解除されず、以下のように二つのエラーが発生したままになってしまいます。
前回のコードではValidatePropertyメソッドでのエラー解除方法が間違っていました。
対象のプロパティのエラーがすべてなくなった場合に、そのプロパティのエラーをすべて解除する、という動作をしていたため、一部のエラーが解除されてもそのままエラー状態が続いてしまいます。
修正内容
SetErrors/ClearErrorsというメソッドを作り、対象のプロパティで発生しているエラーを「一度に複数登録/すべてまとめて解除」という動作ができるようにしています。
この辺、Prism.MvvmのErrorsContainerクラスを参考にしてます。
これで、複数のエラーが同時に発生するような場合にも、適切にエラー表示ができるようになります。
/// <summary> /// 引数で指定されたプロパティに、errorsで指定されたエラーをすべて登録します。 /// </summary> /// <param name="propertyName"></param> /// <param name="errors"></param> protected void SetErrors(string propertyName, IEnumerable<string> errors) { var hasCurrentError = _currentErrors.ContainsKey(propertyName); var hasNewError = errors != null && errors.Count() > 0; if (!hasCurrentError && !hasNewError) return; if (hasNewError) { _currentErrors[propertyName] = new List<string>(errors); } else { _currentErrors.Remove(propertyName); } } /// <summary> /// 引数で指定されたプロパティのエラーをすべて解除します。 /// </summary> /// <param name="propertyName"></param> protected void ClearErrors(string propertyName) { if (_currentErrors.ContainsKey(propertyName)) { _currentErrors.Remove(propertyName); OnErrorsChanged(propertyName); } }
サンプルコード
今回のすべてのコードは以下の通りです。
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> <ItemsControl ItemsSource="{Binding AdornedElement.(Validation.Errors), ElementName=adornedelem}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Foreground="Red" Text="{Binding ErrorContent}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <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}"> </TextBox> </Grid> </Window>
MainWindowViewModel.cs
class MainWindowViewModel : ValidateableBase { private string inputString; [Required(ErrorMessage = "何か入力してください")] [StringLength(10, ErrorMessage = "10文字以内で入力してください")] [RegularExpression("[a-z]+", ErrorMessage = "a-zの文字列を入力してください。")] public string InputString { get { return inputString; } set { this.SetProperty(ref this.inputString, value); } } public MainWindowViewModel() { this.InputString = string.Empty; } }
BindableBase.cs
/// <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)); } } }
ValidatableBase.cs
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); SetErrors(propertyName, errors); } else { ClearErrors(propertyName); } } #region 発生中のエラーを保持する処理を実装 readonly Dictionary<string, List<string>> _currentErrors = new Dictionary<string, List<string>>(); /// <summary> /// 引数で指定されたプロパティに、errorsで指定されたエラーをすべて登録します。 /// </summary> /// <param name="propertyName"></param> /// <param name="errors"></param> protected void SetErrors(string propertyName, IEnumerable<string> errors) { var hasCurrentError = _currentErrors.ContainsKey(propertyName); var hasNewError = errors != null && errors.Count() > 0; if (!hasCurrentError && !hasNewError) return; if (hasNewError) { _currentErrors[propertyName] = new List<string>(errors); } else { _currentErrors.Remove(propertyName); } OnErrorsChanged(propertyName); } /// <summary> /// 引数で指定されたプロパティのエラーをすべて解除します。 /// </summary> /// <param name="propertyName"></param> protected void ClearErrors(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 }
2015/01/29 追記
※SetErrorsを呼んだ時にエラー更新のイベントを発生させておらず、エラーが更新されない可能性がありました。
ValidatableBaseクラスのSetErrorsメソッド最後に以下の一文を追加しています。
OnErrorsChanged(propertyName);