データ検証を実装しようとするときに、System.ComponentModel.DataAnnotations 名前空間の Required や Range が利用できると便利ですよね。そこで、DataAnnotations のデータ検証アトリビュートを利用できる「ValidationViewModelBase」を書いてみました。
ViewModel で次のような感じでデータ検証アトリビュートが使えます。
//
        [Required(ErrorMessage = "この項目は必須項目です。")]
        [Range(minimum: 0, maximum: 130, ErrorMessage = "年齢は 0 から 130 までの数値です。")]
        public string MemoAge
        {
            get { return _age; }
            set
            {
                var propatyName = PropertyHelper.GetName(() => MemoAge);
                base.RemoveItemValidationError(propatyName);    // 項目編集前のエラーメッセージをクリアする
                _age = value;
                base.RaisePropertyChanged(propatyName);
                base.RaisePropertyChanged(PropertyHelper.GetName(() => IsValid));
            }
        }利用上の注意点は、ASP.NET MVC でのコントローラーと違って、ViewModel は View が表示されている限り破棄されないことから、検証エラーの状態が変わるときに「以前の検証エラーメッセージの削除を書かないと、検証エラーが残り続ける」ということです(コード例のコメント参照)。
(2013/01/06 追記)
それからもうひとつ、ModelState クラスが .NET Framework 4.5 では System.Web.ModelBinding 名前空間にありますが、 .NET Framework 4 + ASP.NET MVC 4 では System.Web.Mvc 名前空間(アセンブリは System.Web.Mvc)にあります。このため、.NET Framework 4 上で作成する場合には、ASP.NET MVC 4 が入っていることが必須になります(.NET Framework 4.5 では ASP.NET MVC 4 の機能が内包されています)。この場合、開発 PC 以外の PC に持ち込んで動かす場合には、当該 PC に ASP.NET MVC 4 のランタイムが入っていないと Xaml パースでエラーが発生して動かないというエラー原因を把握しづらい状態になるので注意が必要です。なお。以下のコード例では .NET Framework 4 + ASP.NET MVC 4 上でのものにしています(なので、.NET Framework 4.5 上で作成すると、名前空間が変わってくるところがあるかと。。。)。
クラスの仕様は次のとおりです。
ValidationViewModelBase
プロパティ変更通知及びデータ検証を実装したビューモデルの基底クラス
構文
public abstract class ValidationViewModelBase : INotifyPropertyChanged, IDataErrorInfoプロパティ
- 
bool IsValid
- データ検証エラーの発生の有無を取得します。
- 
ValidationDictionary ViewModelState
- ビューモデルの状態及びバインディングの検証の状態を格納するビューモデル状態ディクショナリ オブジェクトを取得します。
メソッド
- 
void RemoveItemValidationError(string propertyName)
- propertyName に設定されている検証エラーメッセージを削除します。
- 
bool IsPropertyAnnotationError(string propertyName)
- 指定されたプロパティの System.ComponentModel.DataAnnotations のデータ検証アトリビュート検査の結果を確認します。
interface IValidationDictionary
データ検証のインターフェイス
構文
public interface IValidationDictionaryプロパティ
- 
bool IsValid { get; }
- データ検証エラーの発生の有無を取得する。
メソッド
- 
void AddError(string key, string errorMessage)
- データ検証エラーメッセージを追加する。
- 
IEnumerator<KeyValuePair<string, ModelState>> GetEnumerator()
- コレクションを反復処理するために使用できる列挙子を返します。
PropertyHelper
構文
public interface IValidationDictionaryメソッド
- 
static string GetName<T>(Expression<Func<T>> e)
- 引数で渡されたプロパティから当該プロパティの名前を返します。
(2013/12/29 ソースコードのダウンロードページを作成したのでリンクをダウンロードページへ変更)
ソースコードのダウンロードページを作成しました。
まず、前提として、利用する .NET Framework のバージョンは 4 とします。
プロジェクトに次の参照を追加します。
- PresentationCore
- System.ComponentModel.DataAnnotations
- System.Web.Mvc
コードは次のとおりです。
まず、インターフェイスから。「IValidationDictionary」を作成します。
using System.Collections.Generic;
using System.Web.Mvc;
namespace MakCraft.ViewModels
{
    /// <summary>
    /// サービス層とビューモデル層のデータ検証との間のインターフェイス
    /// </summary>
    public interface IValidationDictionary
    {
        /// <summary>
        /// データ検証エラーの発生の有無を取得する。
        /// </summary>
        bool IsValid { get; }
        /// <summary>
        /// データ検証エラーメッセージを追加する。
        /// </summary>
        /// <param name="key">プロパティ名</param>
        /// <param name="errorMessage">エラーメッセージ</param>
        void AddError(string key, string errorMessage);
        /// <summary>
        /// コレクションを反復処理するために使用できる列挙子を返します。
        /// </summary>
        /// <returns></returns>
        IEnumerator<KeyValuePair<string, ModelState>> GetEnumerator();
    }
}次に「Validations」フォルダを作成し、当該フォルダに「ValidationDictionary」を作成します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections;
using System.Web.Mvc;
namespace MakCraft.ViewModels.Validations
{
    public class ValidationDictionary : IDictionary<string, ModelState>, IValidationDictionary
    {
        private readonly Dictionary<string, ModelState> _innerDic = new Dictionary<string, ModelState>(StringComparer.OrdinalIgnoreCase);
        public ValidationDictionary()
        {
        }
        /// <summary>
        /// propertyName に設定されているエラーメッセージを削除します。
        /// </summary>
        /// <param name="key"></param>
        public void RemoveErrorByKey(string propertyName)
        {
            if (_innerDic.ContainsKey(propertyName))
            {
                var errorCollection = this[propertyName];
                errorCollection.Errors.Clear();
                Remove(propertyName);
            }
        }
        /// <summary>
        /// propertyName に対するエラーメッセージを返します。エラーがない場合は null を返します。
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        public string GetValidationError(string propertyName)
        {
            if (!ContainsKey(propertyName))
            {
                return null;
            }
            return this[propertyName].Errors.First().ErrorMessage;
        }
        private ModelState getModelStateForKey(string key)
        {
            if (key == null)
            {
                throw new ArgumentException("key");
            }
            ModelState modelState;
            if (!TryGetValue(key, out modelState))
            {
                modelState = new ModelState();
                this[key] = modelState;
            }
            return modelState;
        }
        #region IvalidationDictionary Members
        public void AddError(string key, string errorMessage)
        {
            getModelStateForKey(key).Errors.Add(errorMessage);
        }
        public bool IsValid
        {
            get { return Values.All(modelState => modelState.Errors.Count == 0); }
        }
        #endregion
        #region IDictionary Members
        public void Add(string key, ModelState value)
        {
            _innerDic.Add(key, value);
        }
        public bool ContainsKey(string key)
        {
            return _innerDic.ContainsKey(key);
        }
        public ICollection<string> Keys
        {
            get { return _innerDic.Keys; }
        }
        public bool Remove(string key)
        {
            return _innerDic.Remove(key);
        }
        public bool TryGetValue(string key, out ModelState value)
        {
            return _innerDic.TryGetValue(key, out value);
        }
        public ICollection<ModelState> Values
        {
            get { return _innerDic.Values; }
        }
        public ModelState this[string key]
        {
            get
            {
                ModelState value;
                _innerDic.TryGetValue(key, out value);
                return value;
            }
            set
            {
                _innerDic[key] = value;
            }
        }
        public void Add(KeyValuePair<string, ModelState> item)
        {
            (_innerDic as IDictionary<string, ModelState>).Add(item);
        }
        public void Clear()
        {
            _innerDic.Clear();
        }
        public bool Contains(KeyValuePair<string, ModelState> item)
        {
            return (_innerDic as IDictionary<string, ModelState>).Contains(item);
        }
        public void CopyTo(KeyValuePair<string, ModelState>[] array, int arrayIndex)
        {
            (_innerDic as IDictionary<string, ModelState>).CopyTo(array, arrayIndex);
        }
        public int Count
        {
            get { return _innerDic.Count; }
        }
        public bool IsReadOnly
        {
            get { return (_innerDic as IDictionary<string, ModelState>).IsReadOnly; }
        }
        public bool Remove(KeyValuePair<string, ModelState> item)
        {
            return (_innerDic as IDictionary<string, ModelState>).Remove(item);
        }
        public IEnumerator<KeyValuePair<string, ModelState>> GetEnumerator()
        {
            return _innerDic.GetEnumerator();
        }
        #endregion IDictionary
        #region IEnumerable Members
        IEnumerator IEnumerable.GetEnumerator()
        {
            return (_innerDic as IEnumerable).GetEnumerator();
        }
        #endregion IEnumerable
    }
}次に、プロパティ名を文字列で埋め込むことを避けるユーティリティを書きます。これは Alexandra Rusina さんが書かれた記事「How can I get objects and property values from expression trees?」から引用しています。「PropertyHelper」を作成します。
using System;
using System.Linq.Expressions;
namespace MakCraft.ViewModels
{
    public static class PropertyHelper
    {
        // http://blogs.msdn.com/b/csharpfaq/archive/2010/03/11/how-can-i-get-objects-and-property-values-from-expression-trees.aspx
        /// <summary>
        /// 引数で渡されたプロパティから当該プロパティの名前を返します。
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="e"></param>
        /// <returns></returns>
        public static string GetName<T>(Expression<Func<T>> e)
        {
            var member = (MemberExpression)e.Body;
            return member.Member.Name;
        }
    }
}次に「RelayCommand」を作成します。
using System;
using System.Windows.Input;
namespace MakCraft.ViewModels
{
    /// <summary>
    /// デリゲートを呼び出すことによって、コマンドを他のオブジェクトに中継する。CanExecute メソッドの既定値は 'true'。
    /// </summary>
    public class RelayCommand : ICommand
    {
        #region fields
        private readonly Action<object> execute;
        private readonly Predicate<object> canExecute;
        #endregion // Fields
        #region Constructor
        /// <summary>
        /// 実行可否判定のないコマンドを作成
        /// </summary>
        /// <param name="execute"></param>
        public RelayCommand(Action<object> execute)
            : this(execute, null)
        {
        }
        /// <summary>
        /// コマンドを作成
        /// </summary>
        /// <param name="execute"></param>
        /// <param name="canExecute"></param>
        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("param: execute");
            this.execute = execute;
            this.canExecute = canExecute;
        }
        #endregion // Constructor
        #region ICommand Members
        public bool CanExecute(object parameter)
        {
            return canExecute == null ? true : canExecute(parameter);
        }
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
        public void Execute(object parameter)
        {
            execute(parameter);
        }
        #endregion // ICommand Members
    }
}最後に「ValidationViewModelBase」を作成します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using MakCraft.ViewModels.Validations;
namespace MakCraft.ViewModels
{
    /// <summary>
    /// プロパティ変更通知及びデータ検証を実装したビューモデルの基底クラス
    /// </summary>
    public abstract class ValidationViewModelBase : INotifyPropertyChanged, IDataErrorInfo
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private ValidationDictionary _validationDic;
        public ValidationViewModelBase()
        {
            _validationDic = new ValidationDictionary();
        }
        /// <summary>
        /// データ検証エラーの発生の有無を取得します。
        /// </summary>
        public bool IsValid
        {
            get { return _validationDic.IsValid; }
        }
        /// <summary>
        /// propertyName に設定されている検証エラーメッセージを削除します。
        /// </summary>
        /// <param name="propertyName"></param>
        public void RemoveItemValidationError(string propertyName)
        {
            _validationDic.RemoveErrorByKey(propertyName);
        }
        /// <summary>
        /// 指定されたプロパティの System.ComponentModel.DataAnnotations のデータ検証アトリビュート検査の結果を確認します。
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns>検証エラーが発生していれば true</returns>
        public bool IsPropertyAnnotationError(string propertyName)
        {
            return (this[propertyName] != null);
        }
        /// <summary>
        /// ビューモデルの状態及びバインディングの検証の状態を格納するビューモデル状態ディクショナリ オブジェクトを取得します。
        /// </summary>
        public ValidationDictionary ViewModelState
        {
            get { return _validationDic; }
        }
        #region IDataErrorInfo Members
        public string Error
        {
            get
            {
                if (IsValid) return string.Empty;
                // インスタンスが持つオブジェクト検証の全結果を連結して返す
                var results = new List<string>();
                foreach (var n in _validationDic)
                {
                    var propertyName = n.Key;
                    results.Add(_validationDic.GetValidationError(propertyName));
                }
                return string.Join(Environment.NewLine, results.Select(n => n));
            }
        }
        public string this[string columnName]
        {
            get
            {
                // System.ComponentModel.DataAnnotations のデータ検証アトリビュートを利用したデータ検証
                var results = new List<ValidationResult>();
                if (!Validator.TryValidateProperty(
                    GetType().GetProperty(columnName).GetValue(this, null),
                    new ValidationContext(this, null, null) { MemberName = columnName },
                    results))
                {
                    results.ForEach(n => _validationDic.AddError(columnName, n.ErrorMessage));
                }
                // ビューモデル状態ディクショナリからエラーメッセージを返す。
                RaisePropertyChanged(PropertyHelper.GetName(() => Error));
                return _validationDic.GetValidationError(columnName);
            }
        }
        #endregion IDataErrorInfo
        protected void RaisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}次回は、この ValidationViewModelBase を利用して作った DateTimePicker を書いてみようと思っています 😉

「DataAnnotations のデータ検証アトリビュートを利用できる ViewModelBase を書いてみました」への4件のフィードバック