その1:http://blogs.wankuma.com/kazuki/archive/2008/11/18/161667.aspx
その2:http://blogs.wankuma.com/kazuki/archive/2008/11/19/161744.aspx
 ちょうど4ヶ月前に、同じネタでBlogを書いてました。
そのときは、BindingValidationErrorイベントとかを使ってやっていました。
 今回は、M-V-VMパターンで作ろうとすると、IDataErrorInfoインターフェースを実装して…というのがWPFでは自然な流れになりそうなので、Silverlightでも試してみようとしたところから話は始まります。
そう、System.ComponentModel.IDataErrorInfoインターフェースは、Silverlight2には入ってませんorz
 ということで、途方に暮れていたらありました。同じこと考えている人が。
Data validation - Silverlight versus WPF part 2
 ふむふむ。
大雑把に感じ取ると、「無ければ作ればいいじゃない?」という精神っぽい。
ということで真似をしてみた。今回は、とりあえずIDataErrorInfoインターフェースを作って、実装するところまでしてみようと思います。
 プロジェクト作成
 ValidationSampleSLという名前で、Silverlightアプリケーションを作成します。
最近Composite Application Guidance for WPF and Silverlight Feb2009を追加するのが日課になってたけど、今回は使わないのでこれでプロジェクト作成の準備はおしまいです。
  
 IDataErrorInfoの作成
 Silverlight2には無いので、作ります。WPFとコードの字面上の互換性をなるべく持たせたいので、まったく同じものを定義します。
  
namespace System.ComponentModel
{
    /// <summary>
    /// Sliverlight2にIDataErrorInfoが無いので作っちゃいます。
    /// これを使うことで、WPFと同じように字面上プログラムを書ける可能性が
    /// あがるかもしれない。(あがらないかもしれない)
    /// </summary>
    public interface IDataErrorInfo
    {
        /// <summary>
        /// エラーがあればエラーを表す文字列を返す
        /// </summary>
        string Error { get; }
        /// <summary>
        /// プロパティ単位でのエラーを表す文字列を返す
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        string this[string propertyName] { get; }
    }
}
ViewModelの作成
続いてViewModelを作成してみます。
ViewModelを作る際に基本となるクラスから作成します。大体いつも同じようなクラスになるんじゃないかな。
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
namespace ValidationSampleSL
{
    /// <summary>
    /// ViewModelを作成するときの基本クラス
    /// </summary>
    public class ViewModelBase : INotifyPropertyChanged, IDataErrorInfo
    {
        #region 検証エラー操作用API
        private Dictionary<string, string> _errors = new Dictionary<string, string>();
        
        /// <summary>
        /// 指定したプロパティにエラー情報をセットする
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="error"></param>
        protected void SetError(string propertyName, string error)
        {
            _errors.Add(propertyName, error);
        }
        
        /// <summary>
        /// 指定したプロパティのエラー情報を消去する
        /// </summary>
        /// <param name="propertyName"></param>
        protected void ClearError(string propertyName)
        {
            if (!_errors.ContainsKey(propertyName))
            {
                return;
            }
            _errors.Remove(propertyName);
        }
        /// <summary>
        /// 指定したプロパティのエラーを取得する
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        protected string GetError(string propertyName)
        {
            string error = null;
            _errors.TryGetValue(propertyName, out error);
            return error;
        }
        
        /// <summary>
        /// 現在エラーがあるプロパティ名の配列を取得する
        /// </summary>
        /// <returns></returns>
        protected string[] GetErrorPropertyNames()
        {
            return _errors.Keys.ToArray();
        }
        /// <summary>
        /// エラーがあるかどうか確認する。エラーがある場合はtrueを返す。
        /// </summary>
        /// <returns></returns>
        public bool HasError()
        {
            return _errors.Count != 0;
        }
        #endregion
        #region IDataErrorInfo メンバ
        public string Error
        {
            get 
            {
                var sb = new StringBuilder();
                foreach (var propertyName in GetErrorPropertyNames())
                {
                    sb.AppendLine(this[propertyName]);
                }
                return sb.ToString();
            }
        }
        public string this[string propertyName]
        {
            get { return GetError(propertyName); }
        }
        #endregion
        #region INotifyPropertyChanged メンバ
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string propertyName)
        {
            var h = PropertyChanged;
            if (h != null)
            {
                h(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        #endregion
    }
}
基本が出来たので、早速ViewModelを作ります。いつも通りのPersonクラスのViewModel版を作ります。
namespace ValidationSampleSL
{
    /// <summary>
    /// 人の情報を表すクラス。名前と年齢をと自己紹介メッセージを持ってる。
    /// </summary>
    public class PersonViewModel : ViewModelBase
    {
        #region 名前プロパティ
        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                // 等しい場合はセットしない
                if (Equals(_name, value))
                {
                    return;
                }
                _name = value;
                ValidateName();
                OnPropertyChanged("Name");
            }
        }
        // 名前の検証 何か入力してね
        private void ValidateName()
        {
            if (string.IsNullOrEmpty(_name))
            {
                SetError("Name", "名前を入力してください");
            }
            else
            {
                ClearError("Name");
            }
        }
        #endregion
        #region 年齢プロパティ
        private string _age;
        public string Age
        {
            get { return _age; }
            set
            {
                // 等しい場合はセットしない
                if (Equals(_age, value))
                {
                    return;
                }
                _age = value;
                ValidateAge();
                OnPropertyChanged("Age");
            }
        }
        // 年齢の検証 整数値で0-120だよ
        private void ValidateAge()
        {
            int resultAge;
            if (string.IsNullOrEmpty(_age))
            {
                SetError("Age", "年齢を入力してください");
            }
            else if (!int.TryParse(_age, out resultAge))
            {
                SetError("Age", "年齢は整数値で入力してください");
            }
            else if (resultAge < 0 || resultAge > 120)
            {
                SetError("Age", "年齢は0~120の間の整数値で入力してください");
            }
            else
            {
                ClearError("Age");
            }
        }
        #endregion
        #region 自己紹介メッセージプロパティ
        public string GreetMessage
        {
            get
            {
                // エラーがあるときは何も自己紹介しない
                if (HasError)
                {
                    return null;
                }
                return "名前は" + Name + "で、" + Age + "才です";
            }
        }
        #endregion
        #region 自己紹介メソッド
        /// <summary>
        /// 自己紹介に変更があったことを通知する
        /// </summary>
        public void Greet()
        {
            OnPropertyChanged("GreetMessage");
        }
        #endregion
    }
}
単調な実装は嫌になってくるけど、とりあえずこんなもんで。
これを表示するためのViewに相当するxamlを作ります。
<UserControl x:Class="ValidationSampleSL.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:l="clr-namespace:ValidationSampleSL"
    Width="400" Height="300">
    <UserControl.DataContext>
        <l:PersonViewModel />
    </UserControl.DataContext>
    <Grid x:Name="LayoutRoot" Background="White">
        <StackPanel Orientation="Vertical">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="名前:" />
                <TextBox Text="{Binding Name, Mode=TwoWay}" Width="250"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="年齢:" />
                <TextBox Text="{Binding Age, Mode=TwoWay}" Width="250"/>
            </StackPanel>
            <Button Content="自己紹介" Click="Button_Click"/>
            <TextBlock Text="{Binding GreetMessage, Mode=TwoWay}" />
        </StackPanel>
    </Grid>
</UserControl>
最後にボタンクリックの処理を追加します。
using System.Windows;
using System.Windows.Controls;
namespace ValidationSampleSL
{
    public partial class Page : UserControl
    {
        public Page()
        {
            InitializeComponent();
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            // 自己紹介してね
            (DataContext as PersonViewModel).Greet();
        }
    }
}
実行してみよう
ぽちっと実行してみます。
因みに、まだ不正な値を入れても何もおきません。
実行直後
 
 
名前と年齢を入れて自己紹介ボタンを押した後
 
 
力尽きた
ということで、今回はここらへんで終わります。
次のエントリで、エラーの結果を画面に表示したりとかといった部分を作っていく予定です。
今回の、範囲でのポイントは以下です。
- IDataErrorInfoインターフェースはないので作りましょう
- ViewModelBaseあたりで必要な実装は作りこんでおきましょう