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

概要

Silverlight 3だとValidatesOnDataErrorsがないので、代替のコントロールを実装してみた。[Silverlight][C#][VBも挑戦]Silverlight2での入力値の検証 その4とほとんど同じ内容。INotifyDataErrorInfoにしてるけど、現状利点なしな使い方。暫定版。

流れ

  1. INotifyDataErrorInfoとINotifyPropertyChangedを実装したViewModelBaseを作成。
  2. ViewModelBaseを継承したMyViewModelをMainPageにバインディング(バインド部分は省略)。
  3. MyViewModelでの検証エラーはViewModelBaseのerrors(INotifyDataErrorInfoのGetErrorsで取得可能)に保持される。
  4. DataErrorControlではDataContext(INotifyDataErrorInfo)を監視して、エラーを取得し、あれば表示。

ファイル一覧

  • Common/INotifyDataErrorInfo
  • Common/DataErrorsChangedEventArgs
  • Common/DataErrorControl
  • Common/ViewModelBase
  • MyViewModel
  • MainPage

暫定版ソースコード

Common/INotifyDataErrorInfo

using System;
using System.Collections;

namespace SilverlightApplicationIDataErrorInfo.Common
{
    public interface INotifyDataErrorInfo
    {
        IEnumerable GetErrors(string propertyName);
        bool HasError { get; }
        event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    }
}

Common/DataErrorsChangedEventArgs

using System;

namespace SilverlightApplicationIDataErrorInfo.Common
{
    public sealed class DataErrorsChangedEventArgs : EventArgs
    {
        public string PropertyName { get; set; }
        public DataErrorsChangedEventArgs(string propertyName)
        {
            this.PropertyName = 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;
            // oldContext.PropertyChanged -= OnDataContextChanged;

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

            UpdateState();
        }

        private void OnErrorsChanged(object sender, DataErrorsChangedEventArgs e)
        {
            if (e.PropertyName == this.PropertyName)
                UpdateState();
        }

        //private void OnDataContextChanged(object sender, PropertyChangedEventArgs e)
        //{
        //    if (e.PropertyName == this.PropertyName)
        //        UpdateState();
        //}
        
        private void UpdateState()
        {
            var errorInfo = this.DataContext as INotifyDataErrorInfo;
            if (errorInfo == null)
            {
                ErrorMessage = null;
                return;
            }
            foreach (var error in errorInfo.GetErrors(this.PropertyName))
            {
                ErrorMessage = error as string;
                return;
            }
            ErrorMessage = null; // 正しくセットできていればクリア
        }

        #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

    }
}

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);
            }
        }
    }
}

MyViewModel

namespace SilverlightApplicationIDataErrorInfo
{
    public class MyViewModel : Common.ViewModelBase
    {

        private string _Name;

        public string Name
        {
            get { return this._Name; }
            set
            {
                this._Name = value;
                Validate("Name", value);
                OnPropertyChanged("Name");
            }
        }

        private void Validate(string propertyName, string value)
        {
            if (propertyName.Equals("Name"))
            {
                if (string.IsNullOrEmpty(value))
                    AddError(propertyName, "required");
                else
                    RemoveError(propertyName, "required");

                if (value.Length > 5)
                    AddError(propertyName, "stringlength < 5");
                else
                    RemoveError(propertyName, "stringlength < 5");
            }
        }   
    }
}

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 Background="White">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Name:" />
            <TextBox Text="{Binding Path=Name, Mode=TwoWay, ValidatesOnExceptions=True}" 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>
</UserControl>