SourceChord

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

WPFでの入力値検証・その3 ~DataAnnotationsを使う~

WPFの入力値検証ネタの続きです。
DataAnnotationsを使い、入力値の検証をシンプルに実装してみます。

前回のサンプルの問題点

ValidatePropertyメソッドで、各プロパティの名前を見てswitchで分岐して判定をしてるのでイケてないです。。。
どうせなら、検証する内容はプロパティの定義部分にまとめて記述した方がスッキリしますよね。

WPFにはDataAnnotationsという仕組みがあり、Validationのための情報を各プロパティに属性としてセットできるので、これを使ってみます。

準備

プロジェクトの参照設定で、System.ComponentModel.DataAnnotationsの参照を追加しておきます。

検証ロジックの修正

以下のように、Validatorクラスのstaticメソッドを用いて、入力値の検証を行います。

        protected void ValidateProperty(string propertyName, object value)
        {
            var context = new ValidationContext(this) { MemberName = propertyName };
            var validationErrors = new List<ValidationResult>();
            if(!Validator.TryValidateProperty(value, context, validationErrors))
            {
                AddError(propertyName, validationErrors.Select(error => error.ErrorMessage).ToString());
            }
            else
            {
                RemoveError(propertyName, null);
            }
        }

検証内容の設定

Validatorでの検証内容は、プロパティに属性を付加することで指定できます。
以下のようにStringLength属性を付けると、文字の長さに応じた入力値の検証ができます。

        private string inputString;
        [StringLength(10, ErrorMessage = "InputStringは10文字以内で入力してください")]
        public string InputString
        {
            get { return inputString; }
            set
            {
                this.SetProperty(ref this.inputString, value);
                ValidateProperty("InputString", value);
            }
        }

その他の検証に使える属性

他にも以下のページにあるようなValidationAttribute派生クラスを使って各種エラーチェックができます。
ValidationAttribute クラス (System.ComponentModel.DataAnnotations)


サンプルコード

ついでなので、_currentErrorsプロパティをDictionary<string, List<string>>型にして、ひとつのプロパティに対して、複数のエラーを保持できるように修正しました。
全コードは以下の通り。

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>
    <Grid>
        <TextBox Width="120"
                 Height="23"
                 Margin="50"
                 HorizontalAlignment="Left"
                 VerticalAlignment="Top"
                 Text="{Binding InputString,
                                UpdateSourceTrigger=PropertyChanged}"/>
    </Grid>
</Window>
MainWindowViewModel.cs
    class MainWindowViewModel : BindableBase, INotifyDataErrorInfo
    {
        private string inputString;
        [StringLength(10, ErrorMessage = "InputStringは10文字以内で入力してください")]
        public string InputString
        {
            get { return inputString; }
            set
            {
                this.SetProperty(ref this.inputString, value);
                ValidateProperty("InputString", value);
            }
        }

        protected void ValidateProperty(string propertyName, object value)
        {
            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
    }