Modelでデータ検証をしてみる

 以前にINotifyDataErrorInfoとSystem.ComponentModel.DataAnnotationsを使ったデータ検証の方法を紹介しましたが、あの時はWPFでデータ検証を行う方法を説明することが目的だったためViewModelでデータ検証を行っていました。(ほかのサイトでもViewModelでやっているのをよく見かけます)
 でもデータ検証はModelで行いたいと思うのが人情(?)です。
 そこでModelでデータ検証をやってみました。
 先ずはデータ検証機能をもったModelと、Modelのデータ検証情報を通知するViewModelを作成します。
 なお、このサンプルコードはLivetを利用しています。
using Livet;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.CompilerServices;

namespace ValidationDemo.Models
{
    public class ValidationModel : NotificationObject, INotifyDataErrorInfo
    {
        // データ検証エラー情報
        protected Dictionary<string, string[]> Errors;

        public ValidationModel()
        {
            Errors = new Dictionary<string, string[]>();
            HasErrors = false;
        }

        #region HasErrors変更通知プロパティ
        private bool _HasErrors;
        // エラーの有無
        public bool HasErrors
        {
            get
            { return _HasErrors; }
            private set
            { 
                if (_HasErrors == value)
                    return;
                _HasErrors = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        // エラー情報が変更された際に発生するイベント
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        // ErrorsChangedイベントを発生させる
        protected virtual void RaiseErrorsChanged([CallerMemberName]string propertyName = "")
        {
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }

        // エラー情報を取得する。
        public IEnumerable GetErrors(string propertyName)
        {
            if (string.IsNullOrWhiteSpace(propertyName)) return null;
            if (!Errors.ContainsKey(propertyName)) return null;
            return Errors[propertyName];
        }

        // 属性を利用した検証を行う
        protected void Validation(object value, [CallerMemberName] string propertyName = "")
        {
            if (string.IsNullOrWhiteSpace(propertyName)) return;

            var results = new List<ValidationResult>();
            string[] messages = null;

            if (!Validator.TryValidateProperty(
                value,
                new ValidationContext(this, null, null) { MemberName = propertyName },
                results))
            {
               messages = results.Select(x => x.ErrorMessage).ToArray();
            }

            UpdateErrors(messages, propertyName);
        }

        // エラー情報(Errors)を更新する。
        protected void UpdateErrors(string[] messages, [CallerMemberName] string propertyName = "")
        {
            if (string.IsNullOrWhiteSpace(propertyName)) return;

            Errors[propertyName] = messages;

            HasErrors = Errors.Values.Any(x => x != null);
            RaiseErrorsChanged(propertyName);
        }
    }
}
 NotificationObjectクラスはLivetが提供しているModelの基底クラスでプロパティの変更通知処理を行ってくれるクラスです。
 データ検証を行う為にINotifyDataErrorInfoを実装し、データ検証用の処理を記載します。
 ここら辺のコードは以前紹介したデータ検証の処理と同じなので説明は省きます。
 System.ComponentModel.DataAnnotationsの属性を利用した検証用にValidationメソッドを、自分でデータ検証を行うとき用にUpdateErrorsメソッドを用意しました。
 HasErrorsプロパティは変更通知プロパティにしてあります。
using Livet;
using Livet.EventListeners;
using System;
using System.Collections;
using System.ComponentModel;
using ValidationDemo.Models;

namespace ValidationDemo.ViewModels
{
    public class ValidationViewModel : ViewModel, INotifyDataErrorInfo
    {
        private ValidationModel Model;

        public ValidationViewModel(ValidationModel model)
        {
            if (model == null) throw new ArgumentNullException(nameof(model));

            Model = model;

            // Modelのプロパティ変更通知にViewModelの変更通知を紐づけ
            CompositeDisposable.Add(new PropertyChangedEventListener(Model)
            {
                (sender, e) => RaisePropertyChanged(e.PropertyName)
            });

            // ModelのErrorsChangedイベントとViewModelのErrorsChangedイベントを紐づけ
            CompositeDisposable.Add(new EventListener<EventHandler<DataErrorsChangedEventArgs>>(
                h => Model.ErrorsChanged += h,
                h => Model.ErrorsChanged -= h,
                (sender, e) => ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(e.PropertyName))));
        }

        public bool HasErrors
        {
            get { return Model.HasErrors; }
        }

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        public IEnumerable GetErrors(string propertyName)
        {
            return Model.GetErrors(propertyName);
        }
    }
}
 ValidationViewModelではINotifyDataErrorInfoを実装し、ValidationModelのデータ検証情報をそのままViewに渡すことができるようにします。

 これでデータ検証機能付きのModelとViewModelが準備できました。
 このクラスを使用してデータ検証をやってみます。
using System.ComponentModel.DataAnnotations;

namespace ValidationDemo.Models
{
    public class Data : ValidationModel
    {

        public Data()
        {
            // エラーになる値を設定
            Value = 500;
            Start = 10;
            End = 1;
        }

        #region Value変更通知プロパティ
        private int _Value;
        [Range(1, 100, ErrorMessage = "1~100の整数を入力してください。")]
        public int Value
        {
            get
            { return _Value; }
            set
            { 
                if (_Value == value)
                    return;
                _Value = value;
                RaisePropertyChanged();

                Validation(value);  // 属性を利用したデータ検証を行う。
            }
        }
        #endregion


        #region Start変更通知プロパティ
        private int _Start;

        public int Start
        {
            get
            { return _Start; }
            set
            { 
                if (_Start == value)
                    return;
                _Start = value;
                RaisePropertyChanged();

                StartAndEndValidation();    // StartとEndの値によるデータ検証
            }
        }
        #endregion


        #region End変更通知プロパティ
        private int _End;

        public int End
        {
            get
            { return _End; }
            set
            { 
                if (_End == value)
                    return;
                _End = value;
                RaisePropertyChanged();

                StartAndEndValidation();    // StartとEndの値によるデータ検証
            }
        }
        #endregion

        // StartとEndの大小関係でデータ検証
        private void StartAndEndValidation()
        {
            var message = Start <= End
                ? null
                : new[] { "Start <= End になるように値を入力してください。" };
            UpdateErrors(message, nameof(Start));
            UpdateErrors(message, nameof(End));
        }
    }
}
 データ検証を行うModelとしてDataクラスをつくりました。
 ValueプロパティはRange属性を利用したデータ検証を行っています。
 データ検証を行う場合はsetterでValidationメソッドを呼び出すだけです。
 StartとEndプロパティは属性を使わず自分でデータ検証を行っています。
 このサンプルではStartとEndの大小関係をチェックしたのち、UpdateErrorsメソッドを呼び出しErrorsを更新します。
using ValidationDemo.Models;

namespace ValidationDemo.ViewModels
{
    public class DataViewModel : ValidationViewModel
    {
        private Data Model;

        public DataViewModel(Data model)
            : base(model)
        {
            Model = model;
        }

        public int Value
        {
            get { return Model.Value; }
            set { Model.Value = value; }
        }

        public int Start
        {
            get { return Model.Start; }
            set { Model.Start = value; }
        }

        public int End
        {
            get { return Model.End; }
            set { Model.End = value; }
        }
    }
}
 ViewModelではValidationViewModelを基底クラスにして、コンストラクタの引数でデータ検証機能付きのModelを受け取るだけです。
 データ検証用のコードは一切描く必要がありません。
 あとは適当にMainWindowViewModelとViewを作って動かしてみます。(一応、コードを載せておきます)
using Livet;
using ValidationDemo.Models;

namespace ValidationDemo.ViewModels
{
    public class MainWindowViewModel : ViewModel
    {
        private Data Model;

        public MainWindowViewModel()
        {
            Model = new Data();
            Data = new DataViewModel(Model);
        }

        public DataViewModel Data { get; }
    }
}
<Window x:Class="ValidationDemo.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:ValidationDemo.Views"
        xmlns:vm="clr-namespace:ValidationDemo.ViewModels"
        Title="MainWindow" Height="100" Width="350">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <Window.Resources>
        <ControlTemplate x:Key="ErrorTemplate">
            <DockPanel>
                <ItemsControl DockPanel.Dock="Right"
                              ItemsSource="{Binding ElementName=adornedElement, Path=AdornedElement.(Validation.Errors)}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>

                <Border BorderBrush="Red" BorderThickness="1"
                        Width="{Binding ElementName=adornedElement, Path=ActualWidth}"
                        Height="{Binding ElementName=adornedElement, Path=ActualHeight}">
                    <AdornedElementPlaceholder Name="adornedElement"/>
                </Border>
            </DockPanel>
        </ControlTemplate>

        <Style TargetType="{x:Type TextBox}">
            <Setter Property="Width" Value="50"/>
            <Setter Property="Validation.ErrorTemplate" Value="{StaticResource ErrorTemplate}"/>
        </Style>
    </Window.Resources>
    
    <Grid DataContext="{Binding Data}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Row="0" Grid.Column="0" Text="値:"/>
        <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Value}"/>

        <TextBlock Grid.Row="1" Grid.Column="0" Text="Start:"/>
        <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Start}"/>

        <TextBlock Grid.Row="2" Grid.Column="0" Text="End:"/>
        <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding End}"/>
    </Grid>
</Window>
このコードを実行すると以下の様になります。
ModelValidation.png
 データ検証機能はきちんと動いています。
 個人的にはModelでデータ検証を行うほうが断然作りやすいと感じました。
スポンサーサイト

テーマ : プログラミング
ジャンル : コンピュータ

データ検証でちょっとはまったお話し

 最近、DataAnnotationsのRange属性を使ったデータ検証でちょっとはまったことがありました。
 0から1までの数値を入力してもらうために以下の様にRange属性を指定しました。
[Range(0, 1, ErrorMessage="0~1の間の数値を入力してください。")]
public double Value1
{
    // 省略
}
 値を入力してみたところ範囲外の数値なのにエラーメッセージが表示されないことがありました。
(具体的には-0.5とか1.3とかを入力してもエラーメッセージが表示されませんでした。)
 何が問題だったのかというとRangeの入力範囲をint型で指定したことが原因でした。
 範囲をint型で指定したため、データ検証もint型で行われていました。
 そのためdouble型の値がint型にキャストされ、小数点以下が切り捨てられた状態で検証を行っていたので1.3等の一部の数値でエラーなしと判断されていました。
 double型のプロパティに対してint型で範囲指定したものとdouble型で範囲指定したものを比較してみました。
RangeValidation.png
 どちらもRange属性で0から1の間の数値を指定するようにしています。
 結果は見ての通り、int型の場合はエラーメッセージが表示されていません。

 考えてみれば当たり前なのですが、地味に解りにくいので気が付くのにちょっと時間がかかりました。

テーマ : プログラミング
ジャンル : コンピュータ

データ検証用の属性を作成する

 今回はSystem.ComponentModel.DataAnnotationsを利用したデータ検証で、オリジナルのデータ検証用属性を作成し使用してみたいと思います。
 サンプルコードでは、文字列の最低長をチェックする属性を作ります。
 まずは、検証用属性を作成します
using System;
using System.ComponentModel.DataAnnotations;

namespace Validation5.CustomValidationAttribute
{
    /// <summary>
    /// 最低文字数を検証する属性
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple=false)]
    public class MinimumStringLengthAttribute : ValidationAttribute
    {

        public MinimumStringLengthAttribute(int minimumLength)
            :base()
        {
            MinimumLength = minimumLength;
        }

        /// <summary>
        /// 文字列の最低長を設定、又は取得する。
        /// </summary>
        public int MinimumLength { get; set; }

        /// <summary>
        /// 検証を行う。
        /// </summary>
        /// <param name="value">値</param>
        /// <returns>検証結果</returns>
        public override bool IsValid(object value)
        {
            if (value == null) return false;

            return value.ToString().Length >= MinimumLength;
        }
    }
}
 オリジナルのデータ検証用属性を作成する場合はValidationAttributeを継承する必要があります。
 属性クラスはお約束として最後にAttributeとつける必要があります。
 AttributeUsage属性でこの属性の性質を指定します。
[AttributeUsage(AttributeTargets.Property, AllowMultiple=false)]
 この属性はプロパティ用の属性なのでAttributeTargetsはPropertyを指定しています。また、最低長の指定を行うのは1プロパティにつき1回だけなのでAllowMultipleをfalseにしています。
 値の検証を行うIsValid(object value)メソッドをオーバーライドし、値のチェック結果を返します。
 これで、オリジナルの検証用属性が完成しました。
 この属性の使用方法は、他のデータ検証用属性の使い方と全く同じです。
 では、この属性を使ってデータ検証を行ってみます。
using Livet;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.CompilerServices;
using Validation5.CustomValidationAttribute;

namespace Validation5.ViewModels
{
    public class MainWindowViewModel : ViewModel, INotifyDataErrorInfo
    {
        private Dictionary<string, string[]> Errors = new Dictionary<string, string[]>();


        #region Value変更通知プロパティ
        private string _Value;
        [MinimumStringLength(5, ErrorMessage="5文字以上入力してください。")]
        public string Value
        {
            get
            { return _Value; }
            set
            { 
                if (_Value == value)
                    return;
                _Value = value;
                RaisePropertyChanged();
                UpdateErrors(value);
            }
        }
        #endregion


        // エラー情報が変更された際に発生するイベント
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        // ErrorsChangedイベントを発生させる
        protected virtual void RaiseErrorsChanged([CallerMemberName]string propertyName = "")
        {
            var h = this.ErrorsChanged;
            if (h != null)
            {
                h(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

        // エラーメッセージを取得する
        public System.Collections.IEnumerable GetErrors(string propertyName)
        {
            if (string.IsNullOrWhiteSpace(propertyName)) return null;
            if (!Errors.ContainsKey(propertyName)) return null;
            return Errors[propertyName];
        }

        // エラーの有無
        public bool HasErrors
        {
            get { return Errors.Values.Any(x => x != null); }
        }

        // エラー情報を更新する。
        private void UpdateErrors(object value, [CallerMemberName]string propertyName = "")
        {
            if (string.IsNullOrWhiteSpace(propertyName)) return;

            var results = new List<ValidationResult>();

            if (Validator.TryValidateProperty(
                value,
                new ValidationContext(this, null, null) { MemberName = propertyName },
                results))
            {
                // エラー無し
                Errors[propertyName] = null;
            }
            else
            {
                Errors[propertyName] = results.Select(x => x.ErrorMessage).ToArray();
            }

            RaiseErrorsChanged(propertyName);
        }
    }
}
 ViewModelでは、先ほど作成したオリジナルの検証用属性を使用しています。
 データ検証の方法は、前回紹介した方法と同じなので説明は省略します。
<Window x:Class="Validation5.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:Validation5.Views"
        xmlns:vm="clr-namespace:Validation5.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <Window.Resources>
        <ControlTemplate x:Key="ErrorTemplate">
            <DockPanel>
                <ItemsControl DockPanel.Dock="Right"
                              ItemsSource="{Binding ElementName=adornedElement, Path=AdornedElement.(Validation.Errors)}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>

                <Border BorderBrush="Red" BorderThickness="1"
                        Width="{Binding ElementName=adornedElement, Path=ActualWidth}"
                        Height="{Binding ElementName=adornedElement, Path=ActualHeight}">
                    <AdornedElementPlaceholder Name="adornedElement"/>
                </Border>
            </DockPanel>
        </ControlTemplate>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        
        <TextBlock Grid.Column="0" Text="文字列:"/>
        <TextBox Grid.Column="1"
                 Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"
                 Validation.ErrorTemplate="{StaticResource ErrorTemplate}"/>
    </Grid>
</Window>
 これを実行し、エラーが出るように値を入力すると以下の様になります。
データ検証5
 こんな感じで簡単にオリジナルの検証用属性が作成できます。

テーマ : プログラミング
ジャンル : コンピュータ

DataAnnotationsを利用してデータ検証を行う

 WPFでデータ検証を行う為の方法を紹介してきましたが、今回はデータ検証をサポートしてくれるSystem.ComponentModel.DataAnnotationsについて紹介します。
 System.ComponentModel.DataAnnotationsにはデータ検証用の属性とデータ検証用属性を使用したデータ検証機構が用意されています。
 データ検証用の属性には以下の様なものがあります。
・RangeAttribute:値の範囲を指定します
・RegularExpressionAttribute:正規表現によるパターンマッチ
・RequiredAttribute:必須(nullや空文字ではないことを示します)
・StringLengthAttribute:文字列の長さ(最大長/最低長)を指定します
 以下のサンプルコードはINotifyDataErrorInfo + DataAnnotationsによるデータ検証の例です。
 (IDataErrorInfoを使っても同じようにデータ検証が可能です。)
using Livet;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.CompilerServices;

namespace Validation4.ViewModels
{
    public class MainWindowViewModel : ViewModel, INotifyDataErrorInfo
    {
        private Dictionary<string, string[]> Errors = new Dictionary<string, string[]>();

        public MainWindowViewModel()
        {
            Value1 = 1;
            Value2 = "a";
        }

        #region Value1変更通知プロパティ
        private int _Value1;

        [Range(1, 10, ErrorMessage="1から10の数値を入力してください。")]
        public int Value1
        {
            get
            { return _Value1; }
            set
            { 
                if (_Value1 == value)
                    return;
                _Value1 = value;
                RaisePropertyChanged();
                UpdateErrors(value);
            }
        }
        #endregion


        #region Value2変更通知プロパティ
        private string _Value2;

        [Required(ErrorMessage="値を入力してください。")]
        public string Value2
        {
            get
            { return _Value2; }
            set
            { 
                if (_Value2 == value)
                    return;
                _Value2 = value;
                RaisePropertyChanged();
                UpdateErrors(value);
            }
        }
        #endregion

        // エラー情報が変更された際に発生するイベント
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        // ErrorsChangedイベントを発生させる
        protected virtual void RaiseErrorsChanged([CallerMemberName]string propertyName = "")
        {
            var h = this.ErrorsChanged;
            if (h != null)
            {
                h(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

        // エラーメッセージを取得する
        public System.Collections.IEnumerable GetErrors(string propertyName)
        {
            if (string.IsNullOrWhiteSpace(propertyName)) return null;
            if (!Errors.ContainsKey(propertyName)) return null;
            return Errors[propertyName];
        }

        // エラーの有無
        public bool HasErrors
        {
            get { return Errors.Values.Any(x => x != null); }
        }

        // エラー情報を更新する。
        private void UpdateErrors(object value, [CallerMemberName]string propertyName = "")
        {
            if (string.IsNullOrWhiteSpace(propertyName)) return;

            var results = new List<ValidationResult>();

            if (Validator.TryValidateProperty(
                value,
                new ValidationContext(this, null, null) { MemberName = propertyName },
                results))
            {
                // エラー無し
                Errors[propertyName] = null;
            }
            else
            {
                Errors[propertyName] = results.Select(x => x.ErrorMessage).ToArray();
            }

            RaiseErrorsChanged(propertyName);
        }
    }
}
 基本はINotifyDataErrorInfoを使用したデータ検証と同じです。
 プロパティが変更された際にデータを検証し、エラーがある場合はErrorsにエラーメッセージを格納します。
 また、ErrorsChangedイベントを発生させてViewにエラー情報の変更を通知します。
 DataAnnotationsを利用したデータ検証では、データ検証を行うプロパティに検証用の属性を付けます。
 Value1には値の範囲を指定するRangeAttributeを、Value2には入力を必須にするためのRequiredAttributeを指定しています。複数の検証用属性を付けることも可能です。
 値の検証はValidatorクラスを使用して行います。このサンプルコードでは値の検証とエラー変更通知を行うUpdateErrorsというメソッドを作成し、そこでデータ検証を行っています。
 CallerMemberName属性を使用して、UppdateErrorsメソッドが呼ばれたメンバー(プロパティ名)を取得するようにしています。
 ValidatorクラスのTryValidatePropertyメソッドを使用して値の検証を行います。
 第1引数には検証する値を、第2引数には条件(検証内容)を、第3引数には検証結果の入れ物を指定します。
 検証内容の情報は、以下のコードで取得しています。
new ValidationContext(this, null, null) { MemberName = propertyName },
 検証の結果、エラーがある場合はfalseを返します。
 エラー情報はresultsに格納されるので、そこからエラーメッセージを取り出しています。

 View側のコードは前回までに紹介した方法と同じです。
<Window x:Class="Validation4.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:Validation4.Views"
        xmlns:vm="clr-namespace:Validation4.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <Window.Resources>
        <ControlTemplate x:Key="ErrorTemplate">
            <DockPanel>
                <ItemsControl DockPanel.Dock="Right" Margin="5,0"
                              ItemsSource="{Binding ElementName=adornedElement, Path=AdornedElement.(Validation.Errors)}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>

                <Border BorderBrush="Red" BorderThickness="1"
                        Width="{Binding ElementName=adornedElement, Path=ActualWidth}"
                        Height="{Binding ElementName=adornedElement, Path=ActualHeight}">
                    <AdornedElementPlaceholder Name="adornedElement"/>
                </Border>
            </DockPanel>
        </ControlTemplate>
    </Window.Resources>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="50"/>
        </Grid.ColumnDefinitions>
        
        <TextBlock Grid.Row="0" Grid.Column="0" Text="数値:"/>
        <TextBox Grid.Row="0" Grid.Column="1"
                 Text="{Binding Value1, UpdateSourceTrigger=PropertyChanged}"
                 Validation.ErrorTemplate="{StaticResource ErrorTemplate}"/>
        
        <TextBlock Grid.Row="1" Grid.Column="0" Text="文字列:"/>
        <TextBox Grid.Row="1" Grid.Column="1"
                 Text="{Binding Value2, UpdateSourceTrigger=PropertyChanged}"
                 Validation.ErrorTemplate="{StaticResource ErrorTemplate}"/>
    </Grid>
</Window>
 このコードを実行し、エラーが出るように値を入力すると以下の様になります。
データ検証4
 データ検証用の属性を複数指定することもできますが、エラーが出現した時点で検証を終了する場合もあるようです。
 そのため、本当なら2つエラーメッセージが出るはずなのに1つしかエラーメッセージが表示されないこともあります。
 今回はDataAnnotationsに用意されている属性を使用しましたが、オリジナルの検証用属性を作成することもできます。
 次回はオリジナルの検証用属性の作り方を紹介します。

テーマ : プログラミング
ジャンル : コンピュータ

INotifyDataErrorInfoを使用したデータ検証

 これまで例外を使用したデータ検証IDataErrorInfoを使用したデータ検証を行いましたが、今回はWPF4.5で加わったINotifyDataErrorInfoを使用したデータ検証を行ってみます。
 IDataErrorInfoを使用したデータ検証と似ていますが、INotifyDataErrorInfoを使用した場合以下の点が異なります。
・HasErrorプロパティを持っている。
・エラー情報の変更をイベントで通知する。
・1つのプロパティに対して複数のエラーメッセージを返すことができる。
・エラー情報はstring型以外でもOK。

 それでは、ViewModelのサンプルコードを見てみましょう。
using Livet;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;

namespace Validation3.ViewModels
{
    public class MainWindowViewModel : ViewModel, INotifyDataErrorInfo
    {
        private Dictionary<string, string[]> Errors = new Dictionary<string, string[]>();

        public MainWindowViewModel()
        {
            Value1 = 0;
            Value2 = "";
        }

        #region Value1変更通知プロパティ
        private int _Value1;

        public int Value1
        {
            get
            { return _Value1; }
            set
            { 
                if (_Value1 == value)
                    return;
                _Value1 = value;

                // データ検証
                Errors["Value1"] = value < 0 ? new[] { "0以上の数値を入力してください。" } : null;
                RaiseErrorsChanged();   // エラー情報が変更されたことを通知

                RaisePropertyChanged();
            }
        }
        #endregion


        #region Value2変更通知プロパティ
        private string _Value2;

        public string Value2
        {
            get
            { return _Value2; }
            set
            { 
                if (_Value2 == value)
                    return;
                _Value2 = value;

                // データ検証
                Errors["Value2"] = string.IsNullOrWhiteSpace(value)
                    ? new[] { "値が入力されていません。", "何か入力してください。" }
                    : null;
                RaiseErrorsChanged();

                RaisePropertyChanged();
            }
        }
        #endregion


        // エラー情報が変更された際に発生するイベント
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        // ErrorsChangedイベントを発生させる
        protected virtual void RaiseErrorsChanged([CallerMemberName]string propertyName = "")
        {
            var h = this.ErrorsChanged;
            if(h != null)
            {
                h(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

        // エラーメッセージを取得する
        public System.Collections.IEnumerable GetErrors(string propertyName)
        {
            if (string.IsNullOrWhiteSpace(propertyName)) return null;
            if (!Errors.ContainsKey(propertyName)) return null;
            return Errors[propertyName];
        }

        // エラーの有無
        public bool HasErrors
        {
            get { return Errors.Values.Any(x => x != null); }
        }
    }
}
 INotifyDataErrorInfoではErrorsChangedイベントでエラー情報の変更を通知します。
 このイベントではどのプロパティのエラー情報が変更されたかが通知されます。
 エラーメッセージはGetErrorsメソッドによって取得されます。このメソッドの戻り値はIEnumerable型なので、string以外のエラー情報を返すことも可能です。(今回はstring型で返していますが)
 IDataErrorInfoには無かったHasErrorプロパティも実装されています。このプロパティによってエラーの有無を取得することが可能になっています。
 エラー情報の受け取り方がIDataErrorInfoと異なっていますが、データ検証の方法はそれほど変わりません。
 今回のコードも、プロパティのsetterでデータ検証を行い、エラーメッセージをErrorsに格納しています。
 エラー情報が複数あっても良いのでErrorsのValueはstring配列にしています。
 そして、エラー情報が変更されたことを通知するためにErrorsChangedイベントを発生させています。
 今回はValue1では今までと同じように1つのエラーメッセージを返していますが、Value2では試しに2つのエラーメッセージを返しています。

 次にView側のコードです。
<Window x:Class="Validation3.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:Validation3.Views"
        xmlns:vm="clr-namespace:Validation3.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <Window.Resources>
        <ControlTemplate x:Key="ErrorTemplate">
            <DockPanel>
                <ItemsControl DockPanel.Dock="Right" Margin="5,0"
                              ItemsSource="{Binding ElementName=adornedElement, Path=AdornedElement.(Validation.Errors)}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
                
                <Border BorderBrush="Red" BorderThickness="1"
                        Width="{Binding ElementName=adornedElement, Path=ActualWidth}"
                        Height="{Binding ElementName=adornedElement, Path=ActualHeight}">
                    <AdornedElementPlaceholder Name="adornedElement"/>
                </Border>
            </DockPanel>
        </ControlTemplate>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="15"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        
        <TextBlock Grid.Row="0" Grid.Column="0" Text="数値:"/>
        <TextBox Grid.Row="0" Grid.Column="1"
                 Validation.ErrorTemplate="{StaticResource ErrorTemplate}"
                 Text="{Binding Value1, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}"/>
        
        <TextBlock Grid.Row="2" Grid.Column="0" Text="文字列:"/>
        <TextBox Grid.Row="2" Grid.Column="1"
                 Validation.ErrorTemplate="{StaticResource ErrorTemplate}"
                 Text="{Binding Value2, UpdateSourceTrigger=PropertyChanged}"/>
    </Grid>
</Window>
 INotifyDataErrorInfoの場合もエラーメッセージをエラーテンプレートを使用して表示しますが、エラーメッセージが複数あることが前提なので前回のコードとは少し異なります。
 エラーメッセージが複数あるのでItemsControlを使ってエラーメッセージを表示しています。
 バインディングの際には「ValidatesOnNotifyDataErrors」をtrueにします。ただ、ValidatesOnNotifyDataErrorsはデフォルトでtrueになっているので省略することが可能です。(Value1では説明のために記載していますが、Value2では省略しています)
 このコードを実行し、エラーを発生させると以下の様になります。
データ検証3
 こんな感じで複数のエラーメッセージを表示できます。
 IDataErrorInfoとINotifyDataErrorInfoどちらを使うかは人それぞれだと思うので、使いやすい方を使えばよいかなと思います。

 WPFでデータ検証を行う方法は、これまでに説明した3種類のどれかを使用することになると思います。
 データ検証はModelで行ったほうが良いのではないかと考える人も多いと思います。ただWPFのデータ検証機能が見ての通りViewModelよりなので、若干の違和感を感じますがViewModelでデータ検証を行っています。
 次回はDataAnnotationsを利用して、もう少し楽にデータ検証を行う方法を紹介します。

テーマ : プログラミング
ジャンル : コンピュータ

カレンダー
10 | 2017/11 | 12
- - - 1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 - -
全記事表示リンク

全ての記事を表示する

カテゴリ
タグリスト

月別アーカイブ
11  09  08  07  06  05  04  03  02  01  12  11  10  09  08  07  06  04  03  02  01  12  11  10  09  08  07  06  05  04  03  02  01  12  11  10  09 
最新記事
リンク
最新コメント
検索フォーム
Amazon