読者です 読者をやめる 読者になる 読者になる

SourceChord

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

WPFでの入力値検証・その2 ~INotifyDataErrorInfoを使ってみる~

随分と時間が空いたけど、、、
今度は、WPF4.5で追加されたINotifyDataErrorInfoを使って、入力値の検証をしてみます。


INotifyDataErrorInfoについては、Silverlightでの説明だけど、以下のサイトが分かりやすい。
TFC Software Blogs - Silverlightの入力値検証のまとめ(1.標準編)
Silverlight 4のデータ検証 - かずきのBlog@hatena
INotifyDataErrorInfo インターフェイス (System.ComponentModel)
IDataErrorInfoでは、バインドしてるプロパティが更新されたタイミングでしか入力値の検証ができないけど、INotifyDataErrorInfoはErrorsChangedイベントを使って任意のタイミングで入力値検証の更新ができるのがポイントみたい。
あとは、ひとつのプロパティに対して、複数のエラーを保持することもできる点とかもメリットかな。

INotifyDataErrorInfoの内容

このインターフェースでは、以下の3つの物が定義されています。

HasErrorsプロパティ 何かエラーが発生しているかを返すbool型のプロパティ
GetErrorsメソッド プロパティ名を指定して、現在発生しているエラーを返すメソッド
ErrorsChangedイベント いずれかのプロパティのエラーに変更があった場合に通知をするイベント(INotifyPropertyChangedのPropertyChangedイベントみたいな感じ)

サンプルコード

MainWindow.xaml

xaml側では、特別なことは何もしてません。
INotifyDataErrorInfoでの入力値検証を有効にするための、ValidatesOnNotifyDataErrorsというプロパティがありますが、このプロパティは規定値がTrueなので、省略しています。

<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

VMでINotifyDataErrorInfoの実装をします。
上記IFの3つのプロパティの他に、Dictionary<string, string>型で、プロパティ名に対して発生中のエラー内容をstringで保持するメンバを用意しました。
(一つのプロパティに複数のエラーを保持できるようにするには、Dictionary<string, List<string>>型として実装すればOK。)
AddError/RemoveErrorメソッドで上記Dictionaryにエラーの追加/削除を行い、ErrorsChangedイベントの発行を行います。

あとはこのVMのプロパティのsetterで、this.SetProperty()をした後にValidatePropertyメソッドを呼びます。
このメソッドには、呼び出したプロパティ名を引数で渡し、対象のプロパティのエラー発生状況を更新します。

    class MainWindowViewModel : BindableBase, INotifyDataErrorInfo
    {
        private string inputString;
        public string InputString
        {
            get { return inputString; }
            set
            {
                this.SetProperty(ref this.inputString, value);
                ValidateProperty("InputString", value);
            }
        }


        protected void ValidateProperty(string propertyName, object value)
        {
            switch (propertyName)
            {
                case "InputString":
                    if (this.InputString.Count() > 10)
                        AddError("InputString", "string is larger than MaxLength");
                    else
                        RemoveError("InputString", null);
                    break;
                default:
                    break;
            }
        }

        #region 発生中のエラーを保持する処理を実装
        readonly Dictionary<string, string> _currentErrors = new Dictionary<string, string>();

        protected void AddError(string propertyName, string error)
        {
            if (!_currentErrors.ContainsKey(propertyName))
                _currentErrors[propertyName] = error;

            OnErrorsChanged(propertyName);
        }

        protected void RemoveError(string propertyName, string error)
        {
            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
    }

追記(7/29)

↑に書いたMainWindowViewModelだと、エラー発生時のエラー内容文字列を正しく保持できていません。
private変数の_currentErrorsを「Dictionary<string, List<string>>」型に変更し、複数のエラー状態を登録できるように、AddError/RemoveErrorメソッドを修正しました。

    class MainWindowViewModel : BindableBase, INotifyDataErrorInfo
    {
        private string inputString;
        public string InputString
        {
            get { return inputString; }
            set
            {
                this.SetProperty(ref this.inputString, value);
                ValidateProperty("InputString", value);
            }
        }


        protected void ValidateProperty(string propertyName, object value)
        {
            switch (propertyName)
            {
                case "InputString":
                    if (this.InputString.Count() > 10)
                        AddError("InputString", "string is larger than MaxLength");
                    else
                        RemoveError("InputString");
                    break;
                default:
                    break;
            }
        }

        #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
    }