How can I define a IDataErrorInfo Error Property for multiple BO properties
Asked Answered
E

3

10

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 ;
        }
    }
Eakin answered 21/1, 2010 at 19:20 Comment(0)
E
11

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();
        }
    }
Eakin answered 21/1, 2010 at 19:50 Comment(2)
This is nice. It would be better to keep errorCollection up to date with the latest error message (if the key already exists, and the message is not null).Appellee
Error property can be optimized to: get { return string.Join(Environment.NewLine, errorCollection.Values); } , if it's empty, it returns only "", which is (as written in the documentation for interface) not considered as an error.Allhallows
R
12

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));
        }       
    }
}
Rockery answered 25/1, 2010 at 8:2 Comment(2)
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.Eakin
This is excellent - coming from the MVC world it's great to be able to use the same convention.Rabble
E
11

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();
        }
    }
Eakin answered 21/1, 2010 at 19:50 Comment(2)
This is nice. It would be better to keep errorCollection up to date with the latest error message (if the key already exists, and the message is not null).Appellee
Error property can be optimized to: get { return string.Join(Environment.NewLine, errorCollection.Values); } , if it's empty, it returns only "", which is (as written in the documentation for interface) not considered as an error.Allhallows
P
1

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.

Penninite answered 21/1, 2010 at 19:31 Comment(1)
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.Eakin

© 2022 - 2024 — McMap. All rights reserved.