It's little disappointing to see about the amount of work we have to do when all we want is a custom error message for the implicit validations done by the default model binder. The reason is the DefaultModelBinder
hides some important methods as private especially the GetValueInvalidResource
and GetValueRequiredResource
. I hope they will take care of this in future.
I was trying to give a generic solution for the problem avoiding to create model binders for every type.
Honestly I haven't tested the below implementation in all the cases(ex. when binding collections) but did in basic levels.
So here is the approach.
We have two custom attributes that helps to pass custom error message for our custom model binder. We could have a base class but that's fine.
public class PropertyValueInvalidAttribute: Attribute
{
public string ErrorMessage { get; set; }
public PropertyValueInvalid(string errorMessage)
{
ErrorMessage = errorMessage;
}
}
public class PropertyValueRequiredAttribute: Attribute
{
public string ErrorMessage { get; set; }
public PropertyValueRequired(string errorMessage)
{
ErrorMessage = errorMessage;
}
}
Here is the model binder this is generic and independent of types and takes care of customizing error messages for both required and invalid validations.
public class ExtendedModelBinder : DefaultModelBinder
{
protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
{
base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
if (propertyDescriptor.Attributes.OfType<PropertyValueInvalidAttribute>().Any())
{
var attr = propertyDescriptor.Attributes.OfType<PropertyValueInvalidAttribute>().First();
foreach (ModelError error in bindingContext.ModelState[propertyDescriptor.Name]
.Errors.Where(err => String.IsNullOrEmpty(err.ErrorMessage) && err.Exception != null)
.ToList())
{
for (Exception exception = error.Exception; exception != null; exception = exception.InnerException)
{
if (exception is FormatException)
{
bindingContext.ModelState[propertyDescriptor.Name].Errors.Remove(error);
bindingContext.ModelState[propertyDescriptor.Name].Errors.Add(attr.ErrorMessage);
break;
}
}
}
}
}
protected override void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
{
if (propertyDescriptor.Attributes.OfType<PropertyValueRequiredAttribute>().Any())
{
var attr = propertyDescriptor.Attributes.OfType<PropertyValueRequiredAttribute>().First();
var isTypeAllowsNullValue = (!propertyDescriptor.PropertyType.IsValueType || Nullable.GetUnderlyingType(propertyDescriptor.PropertyType) != null);
if (value == null && !isTypeAllowsNullValue)
{
bindingContext.ModelState[propertyDescriptor.Name].Errors.Clear();
bindingContext.ModelState.AddModelError(propertyDescriptor.Name, attr.ErrorMessage);
}
}
base.OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, value);
}
}
We are overriding the OnPropertyValidated
method just to override the implicit required error message thrown by the default model binder, and we are overriding the SetProperty
just to use our own message when the type is not valid.
Set our custom binder as the default in Global.asax.cs
ModelBinders.Binders.DefaultBinder = new ExtendedModelBinder();
And you can decorate your properties like this..
[PropertyValueRequired("this field is required")]
[PropertyValueInvalid("type is invalid")]
[Display(Name = "Code Postal")]
public int? CodePostal { get; set; }