Reputation: 4847
What's the best practice of implementing IDataErrorInfo
? Is there anyway to implement it without hard-coded strings for property name?
Upvotes: 12
Views: 6460
Reputation: 51114
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
.
Upvotes: 2
Reputation: 14157
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.
Upvotes: 0
Reputation: 25583
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
}
}
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;
}
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!
Upvotes: 13