Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# validation: IDataErrorInfo without hard-coded strings of property name?

Tags:

c#

validation

wpf

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

like image 446
Shuo Avatar asked Dec 08 '10 02:12

Shuo


2 Answers

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!

like image 96
Nicholas Piasecki Avatar answered Oct 25 '22 07:10

Nicholas Piasecki


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.

like image 33
ProfK Avatar answered Oct 25 '22 06:10

ProfK