Silverlight 3におけるValidatesOnDataErrorsの代替コントロールを実装してみた(暫定版)その2

概要

ねらいとかは前回の記事を参照。今回の注目は2点。

  1. 検証方法をDataAnnotationsによるものに切り替えている点
  2. 検証エラーをプロパティのsetter以外でかけており、それをView(MainPage)に通知できている点

setterでのValidate呼び出しを削除すると分かりやすいです。次あたりErrorMessageプロパティからErrorsプロパティに変更するかもしれない。

MyCommandとかCommandServiceとかは今回の記事には関係がないので割愛。

ソースコード(暫定版)

MainPage

<UserControl
    x:Class="SilverlightApplicationIDataErrorInfo.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"
    xmlns:local="clr-namespace:SilverlightApplicationIDataErrorInfo"
    xmlns:common="clr-namespace:SilverlightApplicationIDataErrorInfo.Common"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    <StackPanel Orientation="Vertical" Background="White">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Name:" Width="100" />
            <TextBox Text="{Binding Path=Name, Mode=TwoWay}" Width="250" />
            <common:DataErrorControl PropertyName="Name">
                <common:DataErrorControl.Template>
                    <ControlTemplate TargetType="common:DataErrorControl">
                        <TextBlock Text="{TemplateBinding ErrorMessage}" />
                    </ControlTemplate>
                </common:DataErrorControl.Template>
            </common:DataErrorControl>
        </StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Message:" Width="100" />
            <TextBlock Text="{Binding Message}" Width="250" />
        </StackPanel>
        <Button
            Content="Update Message"
            common:CommandService.Command="{Binding UpdateMessageCommand}"
            common:CommandService.CommandType="ButtonClick"
            />
    </StackPanel>
</UserControl>

MyViewModel

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using SilverlightApplicationIDataErrorInfo.Common;

namespace SilverlightApplicationIDataErrorInfo
{
    public class MyViewModel : ViewModelBase
    {
        #region Constructors
        
        public MyViewModel()
        {
            this.UpdateMessageCommand = 
                new MyCommand((p) =>
                {
                    Validate("Name");
                    if (this.HasError)
                        return;
                    this.Message = string.Format("Hello, {0}!", this.Name);
                });
        }

        #endregion Constructors

        #region Methods
        
        private void Validate(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName) || propertyName.Equals("Name"))
            {
                ClearError(propertyName);
                var results = new List<ValidationResult>();
                Validator.TryValidateProperty(this.Name,
                    new ValidationContext(this, null, null) { MemberName = propertyName }, results);
                foreach (var result in results)
                {
                    AddError(propertyName, result.ErrorMessage);
                }
            }
        }

        #endregion Methods

        #region Properties
        
        #region Property Name

        private string _Name;

        [Required]
        [StringLength(5)]
        public string Name
        {
            get { return this._Name; }
            set
            {
                this._Name = value;
                Validate("Name");
                OnPropertyChanged("Name");
            }
        }

        #endregion Property Name

        #region Property Message

        private string _Message;
        public string Message
        {
            get { return this._Message; }
            set
            {
                this._Message = value;
                OnPropertyChanged("Message");
            }
        }

        #endregion Property Message

        #region Property UpdateMessageCommand

        public MyCommand UpdateMessageCommand { get; private set; }

        #endregion Property UpdateMessageCommand

        #endregion Properties
    }
}

Common/ViewModelBase

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;

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

        private Dictionary<string, List<string>> errors = new Dictionary<string, List<string>>();

        protected void OnPropertyChanged(string propertyName)
        {
            this.VerityPropertyName(propertyName);
            var handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        [Conditional("DEBUG")]
        [DebuggerStepThrough]
        private void VerityPropertyName(string propertyName)
        {
            Debug.Assert(this.GetType().GetProperty(propertyName) != null,
                "invalid property name: " + propertyName);
        }
    
        public IEnumerable GetErrors(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName)) {
                return this.errors.Values;
            }
            List<string> errorList;
            if (this.errors.TryGetValue(propertyName, out errorList))
            {
                return errorList;
            }
            else
            {
                return new List<string>();
            }
        }

        public bool HasError
        {
            get { return this.errors.Count > 0; }
        }

        private void OnDataErrorsChanged(string propertyName)
        {
            var handler = ErrorsChanged;
            if (handler != null)
            {
                handler(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

        public void AddError(string propertyName, string error)
        {
            if (!errors.ContainsKey(propertyName))
                errors[propertyName] = new List<string>();
            if (!errors[propertyName].Contains(error))
            {
                errors[propertyName].Add(error);
                OnDataErrorsChanged(propertyName);
            }
        }

        public void RemoveError(string propertyName, string errorMessage)
        {
            if (errors.ContainsKey(propertyName) &&
                errors[propertyName].Contains(errorMessage))
            {
                errors[propertyName].Remove(errorMessage);
                if (errors[propertyName].Count == 0)
                {
                    errors.Remove(propertyName);
                }
                OnDataErrorsChanged(propertyName);
            }
        }

        public void ClearError(string propertyName)
        {
            if (errors.ContainsKey(propertyName))
            {
                errors.Remove(propertyName);
                OnDataErrorsChanged(propertyName);
            }
        }
    }
}

Common/DataErrorControl

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace SilverlightApplicationIDataErrorInfo.Common
{
    public class DataErrorControl : Control
    {
        public DataErrorControl()
        {
            DefaultStyleKey = typeof(DataErrorControl);
            SetBinding(MyDataContextProperty, new System.Windows.Data.Binding());
            Foreground = new SolidColorBrush(Colors.Red);
        }

        #region Property PropertyName
        
        public string PropertyName 
        { 
            get
            {
                return (string)GetValue(PropertyNameProperty);
            }
            set
            {
                SetValue(PropertyNameProperty, value);
            }
        }
        
        #endregion Property PropertyName

        #region Property ErrorMessage
        
        public string ErrorMessage
        {
            get
            {
                return (string)GetValue(ErrorMessageProperty);
            }
            private set
            {
                SetValue(ErrorMessageProperty, value);
            }
        }
        
        #endregion Property ErrorMessage

        #region Dependency Property PropertyName

        public static readonly DependencyProperty PropertyNameProperty =
            DependencyProperty.Register(
                "PropertyName", // name
                typeof(string), // propertyType
                typeof(DataErrorControl), // ownerType
                new PropertyMetadata(OnPropertyNameChanged)); // defaultMetadata

        private static void OnPropertyNameChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var self = obj as DataErrorControl;
            if (self != null)
            {
                self.UpdateState();
            }
        }

        #endregion Dependency Property PropertyName

        #region Dependency Property MyDataContext

        private static readonly DependencyProperty MyDataContextProperty =
            DependencyProperty.Register(
                "MyDataContext", // name
                typeof(object), // propertyType
                typeof(DataErrorControl), // ownerType
                new PropertyMetadata(OnMyDataContextChangedStatic)); // defaultMetadata

        private static void OnMyDataContextChangedStatic(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var self = (DataErrorControl)obj;
            self.OnMyDataContextChanged(e);
        }

        private void OnMyDataContextChanged(DependencyPropertyChangedEventArgs e)
        {
            var oldContext = e.OldValue as INotifyDataErrorInfo;
            if (oldContext != null)
                oldContext.ErrorsChanged -= OnErrorsChanged;

            var newContext = e.NewValue as INotifyDataErrorInfo;
            if (newContext != null)
                newContext.ErrorsChanged += OnErrorsChanged;

            UpdateState();
        }

        private void OnErrorsChanged(object sender, DataErrorsChangedEventArgs e)
        {
            if (e.PropertyName == this.PropertyName)
                UpdateState();
        }
        
        private void UpdateState()
        {
            ErrorMessage = null;
            var errorInfo = this.DataContext as INotifyDataErrorInfo;
            if (errorInfo == null)
            {
                return;
            }
            // 先頭要素をエラーメッセージとして設定
            foreach (var error in errorInfo.GetErrors(this.PropertyName))
            {
                ErrorMessage = error as string;
                return;
            }
        }

        #endregion Dependency Property MyDataContext

        #region Dependency Property ErrorMessage

        public static readonly DependencyProperty ErrorMessageProperty =
            DependencyProperty.Register(
                "ErrorMessage", // name
                typeof(string), // propertyType
                typeof(DataErrorControl), // ownerType
                new PropertyMetadata(null)); // defaultMetadata

        #endregion Dependency Property ErrorMessage

    }
}