Modelでデータ検証をしてみる
以前にINotifyDataErrorInfoとSystem.ComponentModel.DataAnnotationsを使ったデータ検証の方法を紹介しましたが、あの時はWPFでデータ検証を行う方法を説明することが目的だったためViewModelでデータ検証を行っていました。(ほかのサイトでもViewModelでやっているのをよく見かけます)
でもデータ検証はModelで行いたいと思うのが人情(?)です。
そこでModelでデータ検証をやってみました。
先ずはデータ検証機能をもったModelと、Modelのデータ検証情報を通知するViewModelを作成します。
なお、このサンプルコードはLivetを利用しています。
データ検証機能はきちんと動いています。
個人的にはModelでデータ検証を行うほうが断然作りやすいと感じました。
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>このコードを実行すると以下の様になります。

スポンサーサイト