Validating Prism IDataErrorInfo with DataAnnotation for ViewModel Objects

I am doing data validation in WPF using Prism MVVM framework. I am using pure data Entities in the ViewModel that are bound to the view layer.

 <TextBox Text="{Binding User.Email, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />

      

I have implemented a generic implementation of IDataErrorInfo in a base ViewModel class that triggers validation for DataAnnotation attributes on my Entity (User in this case).

The problem is that when binding to an Entity, the WPF framework looks for the IDataErrorInfo for the Entity, not the ViewModel where I want this logic to exist. If I wrap my Entity with properties in my ViewModel then everything works, but I don't want to compromise on the use of Entities in the ViewModel.

Is there a way to tell WPF to look for IDataErrorInfo in the ViewModel and not the child that is bound?

Thanks, Mike

+2


source to share


2 answers


The option I went with was to explicitly implement IDataErrorInfo in a base class that extends across all ViewModels and Entities. This seems like the best compromise for getting things looping around WPF, and at least keeps the IDataErrorInfo implementation hidden to callers, so they at least seem clean. I am exposing the protected ValidateProperty, which can be overridden if needed in subclasses for any custom behavior (e.g. Password / PasswordConfirmation script).



public abstract class DataErrorInfo : IDataErrorInfo
{
    string IDataErrorInfo.Error
    {
        get { return null; }
    }

    string IDataErrorInfo.this[string columnName]
    {
        get { return ValidateProperty(columnName); }
    }

    protected virtual string ValidateProperty(string columnName)
    {
         // get cached property accessors
            var propertyGetters = GetPropertyGetterLookups(GetType());

            if (propertyGetters.ContainsKey(columnName))
            {
                // read value of given property
                var value = propertyGetters[columnName](this);

                // run validation
                var results = new List<ValidationResult>();
                var vc = new ValidationContext(this, null, null) { MemberName = columnName };
                Validator.TryValidateProperty(value, vc, results);

                // transpose results
                var errors = Array.ConvertAll(results.ToArray(), o => o.ErrorMessage);
                return string.Join(Environment.NewLine, errors);
            }
            return string.Empty;
    }

    private static readonly Dictionary<string, object> PropertyLookupCache =
        new Dictionary<string, object>();

    private static Dictionary<string, Func<object, object>> GetPropertyGetterLookups(Type objType)
    {
        var key = objType.FullName ?? "";
        if (!PropertyLookupCache.ContainsKey(key))
        {
            var o = objType.GetProperties()
            .Where(p => GetValidations(p).Length != 0)
            .ToDictionary(p => p.Name, CreatePropertyGetter);

            PropertyLookupCache[key] = o;
            return o;
        }
        return (Dictionary<string, Func<object, object>>)PropertyLookupCache[key];
    }

    private static Func<object, object> CreatePropertyGetter(PropertyInfo propertyInfo)
    {
        var instanceParameter = Expression.Parameter(typeof(object), "instance");

        var expression = Expression.Lambda<Func<object, object>>(
            Expression.ConvertChecked(
                Expression.MakeMemberAccess(
                    Expression.ConvertChecked(instanceParameter, propertyInfo.DeclaringType),
                    propertyInfo),
                typeof(object)),
            instanceParameter);

        var compiledExpression = expression.Compile();

        return compiledExpression;
    }

    private static ValidationAttribute[] GetValidations(PropertyInfo property)
    {
        return (ValidationAttribute[])property.GetCustomAttributes(typeof(ValidationAttribute), true);
    }


}

      

+8


source


Of course, I don't know your entire scenario, but I believe that transferring your business entities (or models) with a ViewModel is a big part of the MVVM pattern, especially if you don't have a compatible model (a model that you can link to directly) ... The wrapper can include error management information, as in this scenario, or other things like customizing the display of the model, etc.

However, you can take a look at Prism v4.0 MVVM RI which uses INotifyDataErrorInfo validation and should provide interesting information about validation approaches.



Hope this helps.

Thanks, Damian

+2


source







All Articles