views:

484

answers:

3

I'm starting to implement validation in my WPF project via the IDataErrorInfo interface. My business object contains multiple properties with validation info. How do I get a list of ALL the error messages associated with the object. My thought is that thats what the Error property is for, but I cannot track down anyone using this for reporting on multiple properties.

Thanks!

public string this[string property]
    {
        get {

            string msg = null;
            switch (property)
            {
                case "LastName":
                    if (string.IsNullOrEmpty(LastName))
                        msg = "Need a last name";
                    break;
                case "FirstName":
                    if (string.IsNullOrEmpty(LastName))
                        msg = "Need a first name";
                    break;

                default:
                    throw new ArgumentException(
                        "Unrecognized property: " + property);
            }
            return msg;

        }
    }

    public string Error
    {
        get
        {
            return null ;
        }
    }
+1  A: 

My understanding is that to use this interface, you enumerate the properties on the object, and call the indexer once for each property. It is the caller's responsibility to aggregate any error messages.

codekaizen
Thanks. You're correct. I was looking for a solution that is more encapsulated within the business object, see above. Maybe not the most perfect solution for separation of concerns.
Bob
+1  A: 

Yeah, I see where you could use the indexer. Not a bad way to go I guess. I was really focused on the 'Error' property though. I like the notion of having the errors contained within the business object. I think what I want to do doesnt exist natively, so I just created a dictionary of errors (updated anytime a property changes) on the object and let the Error return a CarriageReturn delimited list of errors, like so :

    public string this[string property]
    {
        get {

            string msg = null;
            switch (property)
            {
                case "LastName":
                    if (string.IsNullOrEmpty(LastName))
                       msg = "Need a last name";
                    break;
                case "FirstName":
                    if (string.IsNullOrEmpty(FirstName))
                        msg = "Need a first name";
                    break;
                default:
                    throw new ArgumentException(
                        "Unrecognized property: " + property);
            }

            if (msg != null && !errorCollection.ContainsKey(property))
                errorCollection.Add(property, msg);
            if (msg == null && errorCollection.ContainsKey(property))
                errorCollection.Remove(property);

            return msg;
        }
    }

    public string Error
    {
        get
        {
            if(errorCollection.Count == 0)
                return null;

            StringBuilder errorList = new StringBuilder();
            var errorMessages = errorCollection.Values.GetEnumerator();
            while (errorMessages.MoveNext())
                errorList.AppendLine(errorMessages.Current);

            return errorList.ToString();
        }
    }
Bob
+3  A: 

I think it is much easier to use the Validation attributes.

class MyBusinessObject {
    [Required(ErrorMessage="Must enter customer")]
    public string Customer { get; set; }

    [Range(10,99, ErrorMessage="Price must be between 10 and 99")]
    public decimal Price { get; set; }

    // I have also created some custom attributes, e.g. validate paths
    [File(FileValidation.IsDirectory, ErrorMessage = "Must enter an importfolder")]
    public string ImportFolder { get; set; }

    public string this[string columnName] {
        return InputValidation<MyBusinessObject>.Validate(this, columnName);
    }

    public ICollection<string> AllErrors() {
        return InputValidation<MyBusinessObject>.Validate(this);
    }
}

The helper class InputValidation looks like this

internal static class InputValidation<T>
    where T : IDataErrorInfo
{
    /// <summary>
    /// Validate a single column in the source
    /// </summary>
    /// <remarks>
    /// Usually called from IErrorDataInfo.this[]</remarks>
    /// <param name="source">Instance to validate</param>
    /// <param name="columnName">Name of column to validate</param>
    /// <returns>Error messages separated by newline or string.Empty if no errors</returns>
    public static string Validate(T source, string columnName) {
       KeyValuePair<Func<T, object>, ValidationAttribute[]> validators;
       if (mAllValidators.TryGetValue(columnName, out validators)) {
           var value = validators.Key(source);
           var errors = validators.Value.Where(v => !v.IsValid(value)).Select(v => v.ErrorMessage ?? "").ToArray();
           return string.Join(Environment.NewLine, errors);
       }
       return string.Empty;
    }

    /// <summary>
    /// Validate all columns in the source
    /// </summary>
    /// <param name="source">Instance to validate</param>
    /// <returns>List of all error messages. Empty list if no errors</returns>
    public static ICollection<string> Validate(T source) {
        List<string> messages = new List<string>();
        foreach (var validators in mAllValidators.Values) {
            var value = validators.Key(source);
            messages.AddRange(validators.Value.Where(v => !v.IsValid(value)).Select(v => v.ErrorMessage ?? ""));
        }
        return messages;
    }

    /// <summary>
    /// Get all validation attributes on a property
    /// </summary>
    /// <param name="property"></param>
    /// <returns></returns>
    private static ValidationAttribute[] GetValidations(PropertyInfo property) {
        return (ValidationAttribute[])property.GetCustomAttributes(typeof(ValidationAttribute), true);
    }

    /// <summary>
    /// Create a lambda to receive a property value
    /// </summary>
    /// <param name="property"></param>
    /// <returns></returns>
    private static Func<T, object> CreateValueGetter(PropertyInfo property) {
        var instance = Expression.Parameter(typeof(T), "i");
        var cast = Expression.TypeAs(Expression.Property(instance, property), typeof(object));
        return (Func<T, object>)Expression.Lambda(cast, instance).Compile();
    }

    private static readonly Dictionary<string, KeyValuePair<Func<T, object>, ValidationAttribute[]>>  mAllValidators;

    static InputValidation() {
        mAllValidators = new Dictionary<string, KeyValuePair<Func<T, object>, ValidationAttribute[]>>();
        foreach (var property in typeof(T).GetProperties()) {
            var validations = GetValidations(property);
            if (validations.Length > 0)
                mAllValidators.Add(property.Name,
                       new KeyValuePair<Func<T, object>, ValidationAttribute[]>(
                         CreateValueGetter(property), validations));
        }       
    }
}
adrianm
This is another good solution, thanks. It worked for me on a VM property that the standard IDataErrorInfo did not. If anyone uses this, need to add a reference to System.ComponentModel.DataAnnotations.
Bob