DataAnnotations のデータ検証アトリビュートを利用できる ViewModelBase を書いてみました

データ検証を実装しようとするときに、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件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。