かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Silverlight 4のデータ検証 「汎用的なINotifyDataErrorInfoの実装」

前に汎用性の欠片も無い感じに実装して実験したINotifyDataErrorInfoですが、System.ComponentModel.DataAnnotationsと連携させる感じで汎用的に実装できます。

とりあえず、INotifyDataErrorInfoとINotifyPropertyChangedを実装したViewModelBaseクラスは、こんな雰囲気になると思います。

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace SilverlightApplication19
{
    public class ViewModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        public System.Collections.IEnumerable GetErrors(string propertyName)
        {
            var result = new List<ValidationResult>();
            // TryValidatePropertyが適任。プロパティの値を取得するのにリフレクションを
            // 使っている部分が気に入らない
            if (Validator.TryValidateProperty(
                GetType().GetProperty(propertyName).GetValue(this, null),
                new ValidationContext(this, null, null) { MemberName = propertyName },
                result))
            {
                return null;
            }
            return result.Select(vr => vr.ErrorMessage);
        }

        // PropertyChangedとErrorChangedイベントを発行する
        protected virtual void OnPropertyChanged(string propertyName)
        {
            RaisePropertyChanged(propertyName);
            RaiseErrorChanged(propertyName);

            // HasErrorsプロパティにも変更があったことを通知する
            RaisePropertyChanged("HasErrors");
        }
        private void RaisePropertyChanged(string propertyName)
        {
            var h = PropertyChanged;
            if (h == null) return;
            h(this, new PropertyChangedEventArgs(propertyName));
        }
        private void RaiseErrorChanged(string propertyName)
        {
            var h = ErrorsChanged;
            if (h == null) return;
            h(this, new DataErrorsChangedEventArgs(propertyName));
        }

        // オブジェクトにエラーがあったらtrue返す
        public bool HasErrors
        {
            get 
            {
                var dummy = new List<ValidationResult>();
                // TryValidateObjectが適任
                return !Validator.TryValidateObject(
                    this,
                    new ValidationContext(this, null, null),
                    dummy);
            }
        }
    }
}

これを使う側では、普通にプロパティを定義して、DataAnnotationsにある属性をつけるだけです。

using System.ComponentModel.DataAnnotations;

namespace SilverlightApplication19
{
    public class PersonViewModel : ViewModelBase
    {
        private string _fullName;
        [Required(ErrorMessage="名前を入力してください")]
        public string FullName
        {
            get { return _fullName; }
            set
            {
                _fullName = value;
                OnPropertyChanged("FullName");
            }
        }
    }
}

適当に、このPersonViewModelを表示する画面を作って動きを確認します。

<UserControl 
    x:Class="SilverlightApplication19.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    xmlns:local="clr-namespace:SilverlightApplication19"
    d:DesignHeight="300" d:DesignWidth="400">
    <UserControl.DataContext>
        <local:PersonViewModel FullName="田中 太郎" />
    </UserControl.DataContext>
    <Grid x:Name="LayoutRoot" Background="White">
        <TextBlock HorizontalAlignment="Left" Margin="12,12,0,0" Name="textBlock1" Text="FullName" VerticalAlignment="Top" />
        <TextBox Text="{Binding Path=FullName, Mode=TwoWay}" Height="24" HorizontalAlignment="Left" Margin="72,8,0,0" Name="textBox1" VerticalAlignment="Top" Width="148" />
        <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="12,38,0,0" Name="button1" VerticalAlignment="Top" Width="75" />
    </Grid>
</UserControl>

実行すると、以下のような画面が起動します。

必須入力にしてるので、テキストボックスを空っぽにするとエラーが表示されます。

いい感じかも。