SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

WPFでの入力値検証・その5 ~INotifyDataErrorInfo実装のベースクラスを作成~

(10/20追記)

この記事で書いたValidateableBaseクラスでは、複数のエラーが発生した時に正しくエラー状態を保持できません。
↓に続きを書きました。
WPFでの入力値検証・その6 ~複数のエラー表示への対応~ - SourceChord


立て続けにWPFでの入力検証ネタを続けましたが、
INotifyDataErrorInfoを実装するには結構なコード量が必要になります。
また、毎度同じようなコードの記述をすることになります。

ということで、INotifyDataErrorInfo実装のための、ベースクラスを作ってみました。
ValidateableBaseというクラスを作って、そっちにINotifyDataErrorInfoの実装をまとめています。


以下のクラス図のような継承関係です。
f:id:minami_SC:20140614203425p:plain:w400


こうすることで、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>