かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

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>

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

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

いい感じかも。