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