views:

1135

answers:

2

I'm experiencing some different behavior after switching from ASP.NET MVC 1.0 to ASP.NET MVC 2 Beta. I checked the breaking changes but it's not clear where the issue lies.

The problem has to do with the default model binder and a model that implements IDataErrorInfo.

The property (IDataErrorInfo.Item):

public string this[string columnName]

is no longer being called for each property. What am I missing?

+1  A: 

DefaultModelBinder in MVC 1.0:

protected virtual void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
{
    IDataErrorInfo model = bindingContext.Model as IDataErrorInfo;
    if (model != null)
    {
        string str = model[propertyDescriptor.Name];
        if (!string.IsNullOrEmpty(str))
        {
            string key = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
            bindingContext.ModelState.AddModelError(key, str);
        }
    }
}

DefaultModelBinder in MVC 2.0 beta:

protected virtual void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
{
    ModelMetadata metadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
    metadata.Model = value;
    string prefix = CreateSubPropertyName(bindingContext.ModelName, metadata.PropertyName);
    foreach (ModelValidator validator in metadata.GetValidators(controllerContext))
    {
        foreach (ModelValidationResult result in validator.Validate(bindingContext.Model))
        {
            bindingContext.ModelState.AddModelError(CreateSubPropertyName(prefix, result.MemberName), result.Message);
        }
    }
    if ((bindingContext.ModelState.IsValidField(prefix) && (value == null)) && !TypeHelpers.TypeAllowsNullValue(propertyDescriptor.PropertyType))
    {
        bindingContext.ModelState.AddModelError(prefix, GetValueRequiredResource(controllerContext));
    }
}

It doesn't use IDataErrorInfo this[string columnName] property... Seems like a bug, because DefaultModelBinder still uses Error property. It is inconsistency at least.

EDIT

I used reflector and noticed that DataErrorInfoPropertyModelValidator doesn't seem to be used, so I created my own class:

public class DataErrorInfoModelPropertyValidatorProvider : ModelValidatorProvider
{
    // Methods
    public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
    {
        if (metadata == null)
        {
            throw new ArgumentNullException("metadata");
        }
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        var validators = new List<ModelValidator>();
        validators.Add(new DataErrorInfoPropertyModelValidator(metadata, context));
        return validators;
    }

    internal sealed class DataErrorInfoPropertyModelValidator : ModelValidator
    {
        // Methods
        public DataErrorInfoPropertyModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
            : base(metadata, controllerContext)
        {
        }

        public override IEnumerable<ModelValidationResult> Validate(object container)
        {
            if (container != null)
            {
                IDataErrorInfo info = container as IDataErrorInfo;
                if (info != null)
                {
                    string str = info[Metadata.PropertyName];
                    if (!string.IsNullOrEmpty(str))
                    {
                        ModelValidationResult[] resultArray = new ModelValidationResult[1];
                        ModelValidationResult result = new ModelValidationResult();
                        result.Message = str;
                        resultArray[0] = result;
                        return resultArray;
                    }
                }
            }
            return Enumerable.Empty<ModelValidationResult>();
        }
    }
}

Then I used:

ModelValidatorProviders.Providers.Add(new DataErrorInfoModelPropertyValidatorProvider());

And it works:) This is just temporary solution. Will have to be corrected in final MVC 2.

EDIT

I also changed if (base.Metadata.Model != null) to if (container != null) in Validate() method of DataErrorInfoPropertyModelValidator.

LukLed
The call to metaData.GetValidators should yield a DataErrorInfoPropertyModelValidator. Calling its Validate should result in a direct call to the IDataErrorInfo interface. It just doesn't seem to be making it that far.
SevenCentral
+1  A: 

After some further debugging work I believe I understand why in my particular case the IDataErrorInfo.Item isn't being called. The following code is used in the ASP.NET MVC 2 Beta to validate an IDataErrorInfo property:

internal sealed class DataErrorInfoPropertyModelValidator : ModelValidator
{
    public DataErrorInfoPropertyModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
        : base(metadata, controllerContext)
    {
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
        if (Metadata.Model != null)
        {
            var castContainer = container as IDataErrorInfo;
            if (castContainer != null)
            {
                string errorMessage = castContainer[Metadata.PropertyName];
                if (!String.IsNullOrEmpty(errorMessage))
                {
                    return new[] { new ModelValidationResult { Message = errorMessage } };
                }
            }
        }
        return Enumerable.Empty<ModelValidationResult>();
    }
}

My model contains a property that is System.Nullable<int> and when the model binder binds an empty string from the HTML post, the Metadata.Model is equal to null, thus the validation doesn't run.

This is fundamentally different from ASP.NET MVC 1.0, where this scenario fires the validator all the way through to a call to IDataErrorInfo.Item.

Am I just not using something the way it was intended?

SevenCentral
Thanks for reporting this. I talked with some of the other MVC devs, and we suspect the null check served a purpose at some point between Preview 2 and Beta but has since been rendered unnecessary. I filed a bug to remove the check.
Levi
I appreciate you looking into this!
SevenCentral