What's the best practice of implementing IDataErrorInfo
? Is t开发者_运维百科here anyway to implement it without hard-coded strings for property name?
A base class for common validation routines
You can use DataAnnotations if you do some futzing in the IDataErrorInfo
implementation. For example, here is a base view model that I use frequently (from Windows Forms, but you can extrapolate):
public class ViewModelBase : IDataErrorInfo, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public SynchronizationContext Context
{
get;
set;
}
public bool HasErrors
{
get
{
return !string.IsNullOrWhiteSpace(this.Error);
}
}
public string Error
{
get
{
var type = this.GetType();
var modelClassProperties = TypeDescriptor
.GetProperties(type)
.Cast();
return
(from modelProp in modelClassProperties
let error = this[modelProp.Name]
where !string.IsNullOrWhiteSpace(error)
select error)
.Aggregate(new StringBuilder(), (acc, next) => acc.Append(" ").Append(next))
.ToString();
}
}
public virtual string this[string columnName]
{
get
{
var type = this.GetType();
var modelClassProperties = TypeDescriptor
.GetProperties(type)
.Cast();
var errorText =
(from modelProp in modelClassProperties
where modelProp.Name == columnName
from attribute in modelProp.Attributes.OfType()
from displayNameAttribute in modelProp.Attributes.OfType()
where !attribute.IsValid(modelProp.GetValue(this))
select attribute.FormatErrorMessage(displayNameAttribute == null ? modelProp.Name : displayNameAttribute.DisplayName))
.FirstOrDefault();
return errorText;
}
}
protected void NotifyPropertyChanged(string propertyName)
{
if (string.IsNullOrWhiteSpace(propertyName))
{
throw new ArgumentNullException("propertyName");
}
if (!this.GetType().GetProperties().Any(x => x.Name == propertyName))
{
throw new ArgumentException(
"The property name does not exist in this type.",
"propertyName");
}
var handler = this.PropertyChanged;
if (handler != null)
{
if (this.Context != null)
{
this.Context.Post(obj => handler(this, new PropertyChangedEventArgs(propertyName)), null);
}
else
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
An example usage:
public class LogOnViewModel : ViewModelBase
{
[DisplayName("User Name")]
[Required]
[MailAddress] // This is a custom DataAnnotation I wrote
public string UserName
{
get
{
return this.userName;
}
set
{
this.userName = value;
this.NotifyPropertyChanged("UserName");
}
}
[DisplayName("Password")]
[Required]
public string Password
{
get; // etc
set; // etc
}
}
Taking advantage of IDataErrorInfo for one-off validation routines
To be honest, I end up using both annotations and the switch. I use the annotations for the simple validations, and if I have more complicated ones (such as "only validate this property if this other property is set"), then I will resort to the switch in an override of the this[]
index. That pattern frequently looks like this (just a made up example, it doesn't have to make sense:
public override string this[string columnName]
{
get
{
// Let the base implementation run the DataAnnotations validators
var error = base[columnName];
// If no error reported, run my custom one-off validations for this
// view model here
if (string.IsNullOrWhiteSpace(error))
{
switch (columnName)
{
case "Password":
if (this.Password == "password")
{
error = "See an administrator before you can log in.";
}
break;
}
}
return error;
}
Some opinions
As for specifying property names as strings: you could do fancy thing with lambdas, but my honest advice is to just get over it. You might note that in my ViewModelBase
, my little NotifyPropertyChanged
helper does some reflection magic to make sure I haven't fat-fingered a property name--it helps me detect a data binding error quickly rather than run around for 20 minutes figuring out what I've missed.
Your application is going to have a spectrum of validation, from piddly things like "required" or "max length" down at the UI property level to "required only if something else is checked" at a different UI level and all the way up to "username does not exist" in the domain/persistence level. You will find that you will have to make trade-offs between repeating a little validation logic in the UI versus adding lots of metadata in the domain to describe itself to the UI, and you'll have to make trade-offs as to how these different classes of validation errors are displayed to the user.
Hope that helps. Good luck!
You may find some use in the accepted answer to my question, Select a model property using a lambda and not a string property name, specifically only for specifying properties without using strings. I'm afraid I can't help directly with implementing IDataErrorInfo
.
For this situation (and INotifyPropertyChanged
) I tend to go with a private static class declaring all the property names as constants:
public class Customer : INotifyPropertyChanging, INotifyPropertyChanged, IDataErrorInfo, etc
{
private static class Properties
{
public const string Email = "Email";
public const string FirstName = "FirstName";
}
}
There's still a little repetition but it's worked fine for me on a few projects.
As for organizing the validation... You could consider a separate CustomerValidator class to be supplied at run-time. You can then swap different implementations for different contexts. So, for example, new customers could be validated differently to existing ones without a mess of conditionals.
精彩评论