12

What's the best practice of implementing IDataErrorInfo? Is there anyway to implement it without hard-coded strings for property name?

Shuo
  • 4,749
  • 9
  • 45
  • 63
  • This interface is used in Windows Forms data binding. Getting property names (columns, really) isn't much of a problem there. Are you sure you want to use it in a WPF project? – Hans Passant Dec 08 '10 at 03:10
  • 6
    @Hans: yes, I think people are also using it for WPF: http://stackoverflow.com/questions/63646/wpf-data-binding-and-validation-rules-best-practices/63754#63754 – Shuo Dec 08 '10 at 03:12

3 Answers3

13

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!

Nicholas Piasecki
  • 25,203
  • 5
  • 80
  • 91
  • Thanks very much. A not-related question: could you please give me some background on why we need `SynchronizationContext` and how it should be used? I'm not very familiar with concurrency programming but curious about that. :) – Shuo Dec 08 '10 at 03:44
  • 1
    WPF and Windows Forms controls and elements are not thread-safe. This means that when you interact with them, you need to be on the same thread that they were created on, a thread with a message pump. If you fire off to do some work in a background thread, and then touch one of these INotifyPropertyChanged properties of a view model, then the event will fire and the control will try to update itself as a result of an event on the wrong thread. The Windows Forms implementation of SynchronizationContext allows us to marshal that callback to the correct thread. If confused, leave it out. – Nicholas Piasecki Dec 08 '10 at 13:00
  • 1
    For WPF you should be using Dispatcher instead of SynchronizationContext. – John Bowen Dec 08 '10 at 13:30
  • +1 for using "fat-fingered". I'm totally going to add that to my base classes! – Rossco Jun 22 '14 at 22:23
2

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.

Community
  • 1
  • 1
ProfK
  • 49,207
  • 121
  • 399
  • 775
0

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.

Andrew Kennan
  • 13,947
  • 3
  • 24
  • 33