Livet ViewModel のValidation で ValidationGroup を作成する

LivetというかWPFでの単項目のチェックの実装について悩んでいた時、Nine Works: WPFでのデータ検証 1:IDataErrorInfoを使ったデータ検証 というエントリを見つけたので参考にさせて頂きました。
しかし、実案件に導入した際、ASP.NET の ValidationGroup のようにValidation対象をグループ化したいという要望が出てきました。しかし、DataAnotation にはValidationGroup に該当するものを見つける事ができませんでした。そのため、DiplayAttribute に存在する GroupName を ValidationGroup として利用する機能を追加します。

環境

補足

  • DisplayAttribute のメンバである、Groupnameを利用しますが、このプロパティの説明「UI でのフィールドのグループ化に使用される値を取得または設定します。」に則った利用法なのか調べたかわかりませんでした。利用時は、十分検証してくださいい。

追加した機能

  • プロパティをグループ化し、検証時指定グループの検証結果だけ取得する。エラーメッセージも同様。
  • 1つのプロパティに複数グループの指定もカンマ区切りで指定可能。
  • isValidメソッドを利用した検証時、Errorプロパティに検証結果のメッセージが設定される

検証を行うViewModelクラス

// using 省略

namespace LivetWPFApplication.ViewModels
{

    /// <summary>
    /// DataAnnotationによるプロパティの検証を可能とするViewModelです。
    /// </summary>
    public class ValidationViewModel : ViewModel, IDataErrorInfo
    {
        protected Dictionary<string, string> _errors = new Dictionary<string, string>();

        /// <summary>
        /// オブジェクトに関する間違いを示すエラー メッセージを取得します。
        /// </summary>
        public string Error
        {
            get
            {
                return GetErrorMessage(String.Empty);
            }
        }

        /// <summary>
        /// DataAnnotationsを使ってデータ検証を行い
        /// 指定した名前のプロパティに関するエラー メッセージを取得します。
        /// </summary>
        /// <param name="propName"></param>
        /// <returns></returns>
        public string this[string propName]
        {
            get
            {
                var result = new List<ValidationResult>();
                if (Validator.TryValidateProperty(
                        GetType().GetProperty(propName).GetValue(this, null),
                        new ValidationContext(this, null, null) { MemberName = propName },
                        result))
                {
                    // エラー無し
                    _errors[propName] = null;
                }
                else
                {
                    _errors[propName] = result.First().ErrorMessage;
                }

                return _errors[propName];
            }
        }

        /// <summary>
        /// 指定したグループに属するプロパティの値が妥当かを返します。
        /// </summary>
        /// <param name="validationGroupName">検証を行うグループ</param>
        /// <returns></returns>
        public bool isValid(string validationGroupName)
        {
            var errorMessages = GetError(validationGroupName);
            // エラーメッセージの更新通知
            RaiseErrorChanged();
            return errorMessages.Count() == 0;
        }

        /// <summary>
        ///  指定したグループに属するプロパティのエラーメッセージのサマリを取得します。
        /// </summary>
        /// <param name="validationGroupName">検証を行うグループ</param>
        /// <returns></returns>
        public string GetErrorMessage(string validationGroupName)
        {
            var errorMessages = GetError(validationGroupName);
            return String.Join(System.Environment.NewLine, errorMessages.Values);
        }

        /// <summary>
        /// オブジェクトのすべてのプロパティの値が妥当かを返します。
        /// </summary>
        /// <returns></returns>
        public bool isValid()
        {
            return isValid(String.Empty);
        }

        // Errorの変更通知
        protected virtual void RaiseErrorChanged()
        {
            RaisePropertyChanged("Error");
        }


        /// <summary>
        /// 指定したグループのエラーを取得します。
        /// </summary>
        /// <param name="validationGroupName"></param>
        /// <returns>エラーのプロパティ名、エラーメッセージ</returns>
        private Dictionary<string, string> GetError(string validationGroupName)
        {
            IEnumerable<KeyValuePair<string, string>> errorMessages;

            if (String.IsNullOrEmpty(validationGroupName))
            {
                errorMessages = _errors.Where(x => !String.IsNullOrEmpty(x.Value));
            }
            else
            {
                errorMessages = _errors.Where(x =>
                        IsContaindValidationGroup(x.Key, validationGroupName) && _errors[x.Key] != null);
            }

            return errorMessages.ToDictionary(x => x.Key, x => x.Value);

        }

        /// <summary>
        /// 対象プロパティが、指定したグループに属するかの結果を返します。
        /// </summary>
        /// <param name="propertyName">対象プロパティ名</param>
        /// <param name="validationGroupName">指定を行うグループ</param>
        /// <returns></returns>
        private bool IsContaindValidationGroup(string propertyName, string validationGroupName)
        {
            var attributes =
                    Attribute.GetCustomAttributes(GetType().GetProperty(propertyName), typeof(DisplayAttribute));

            if (attributes.Count() == 0)
            {
                return false;
            }
            var displayAttr = attributes[0] as DisplayAttribute;

            return displayAttr.GetGroupName().Split(',').ToList().Exists(x => x.Trim() == validationGroupName);
        }

    }

}

Validationを行うViewModelの実装例

public class TargetVM : ValidationViewModel
{

    #region Value1変更通知プロパティ
    private string _Value1;

    [Display(Name = "値1", GroupName = "grp1")]
    [Required]
    public string Value1
    {
        get
        { return _Value1; }
        set
        {
            if (_Value1 == value)
                return;
            _Value1 = value;
            RaisePropertyChanged("Value1");
        }
    }
    #endregion


    #region Value2変更通知プロパティ
    private int? _Value2;

    [Required]
    [Display(Name = "値2", GroupName = "grp2")]
    public int? Value2
    {
        get
        { return _Value2; }
        set
        {
            if (_Value2 == value)
                return;
            _Value2 = value;
            RaisePropertyChanged("Value2");
        }
    }
    #endregion


    #region Value3変更通知プロパティ
    private string _Value3;

    [Required]
    [Display(GroupName="grp3,grp4")]
    public string Value3
    {
        get
        { return _Value3; }
        set
        { 
            if (_Value3 == value)
                return;
            _Value3 = value;
            RaisePropertyChanged("Value3");
        }
    }
    #endregion


    public bool Click()
    {
        return isValid("grp1");
    }

    public bool ClickGroup2()
    {
        return isValid("grp2");
    }

    public bool ClickGroup3()
    {
        return isValid("grp3");
    }

    public bool ClickGroup4()
    {
        return isValid("grp4");
    }

    public bool ClickGroupAll()
    {
        return isValid();
    }
}