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?
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.
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?
Related
How do you call the default model binder within Web API in a custom IModelBinder? I know MVC has a default binder, but I can't use it with Web API. I just want to use the default Web API binder, and then run some custom logic after that (to avoid re-inventing the wheel).
public class CustomBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
// Get default binding (can't mix Web API and MVC)
var defaultMvcBinder = System.Web.ModelBinding.ModelBinders.Binders.DefaultBinder;
var result = defaultMvcBinder.BindModel(actionContext, bindingContext); // Won't work
if (result == false) return false;
// ... set additional model properties
return true;
}
}
In case others stumble on this question, I had to implement the custom model binder with activation context since there is nothing to re-use from Web API. Here is the solution I am using for my limited scenarios that needed to be supported.
Usage
The implementation below allows me to let any model optionally use JsonProperty for model binding, but if not provided, will default to just the property name. It supports mappings from standard .NET types (string, int, double, etc). Not quite production ready, but it meets my use cases so far.
[ModelBinder(typeof(AttributeModelBinder))]
public class PersonModel
{
[JsonProperty("pid")]
public int PersonId { get; set; }
public string Name { get; set; }
}
This allows the following query string to be mapped in a request:
/api/endpoint?pid=1&name=test
Implementation
First, the solution defines a mapped property to track the source property of the model and the target name to use when setting the value from the value provider.
public class MappedProperty
{
public MappedProperty(PropertyInfo source)
{
this.Info = source;
this.Source = source.Name;
this.Target = source.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName ?? source.Name;
}
public PropertyInfo Info { get; }
public string Source { get; }
public string Target { get; }
}
Then, a custom model binder is defined to handle the mapping. It caches the reflected model properties to avoid repeating the reflection on subsequent calls. It may not be quite production ready, but initial testing has been promising.
public class AttributeModelBinder : IModelBinder
{
public static object _lock = new object();
private static Dictionary<Type, IEnumerable<MappedProperty>> _mappings = new Dictionary<Type, IEnumerable<MappedProperty>>();
public IEnumerable<MappedProperty> GetMapping(Type type)
{
if (_mappings.TryGetValue(type, out var result)) return result; // Found
lock (_lock)
{
if (_mappings.TryGetValue(type, out result)) return result; // Check again after lock
return (_mappings[type] = type.GetProperties().Select(p => new MappedProperty(p)));
}
}
public object Convert(Type target, string value)
{
try
{
var converter = TypeDescriptor.GetConverter(target);
if (converter != null)
return converter.ConvertFromString(value);
else
return target.IsValueType ? Activator.CreateInstance(target) : null;
}
catch (NotSupportedException)
{
return target.IsValueType ? Activator.CreateInstance(target) : null;
}
}
public void SetValue(object model, MappedProperty p, IValueProvider valueProvider)
{
var value = valueProvider.GetValue(p.Target)?.AttemptedValue;
if (value == null) return;
p.Info.SetValue(model, this.Convert(p.Info.PropertyType, value));
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
try
{
var model = Activator.CreateInstance(bindingContext.ModelType);
var mappings = this.GetMapping(bindingContext.ModelType);
foreach (var p in mappings)
this.SetValue(model, p, bindingContext.ValueProvider);
bindingContext.Model = model;
return true;
}
catch (Exception ex)
{
return false;
}
}
}
I've a telerik mvc menu that binds to a sitemap. All links are properly setup and it binds successfully. But in the post methods, if model is posted back to the view for invalid models using View(model), it breaks with below error. It works fine with PRG pattern i.e., if I redirect to view.
INFO[System.NullReferenceException: Object reference not set to an
instance of an object. at
Telerik.Web.Mvc.Infrastructure.Implementation.ControllerAuthorization.IsAccessibleToUser(RequestContext
requestContext, String controllerName, String actionName,
RouteValueDictionary routeValues) in f:\109\Griffin\Trunk
Full\Sources\Source\Telerik.Web.Mvc\Infrastructure\Implementation\ControllerAuthorization.cs:line
75 at
Telerik.Web.Mvc.Infrastructure.Implementation.NavigationItemAuthorization.IsAccessibleToUser(RequestContext
requestContext, INavigatable navigationItem) in f:\109\Griffin\Trunk
Full\Sources\Source\Telerik.Web.Mvc\Infrastructure\Implementation\NavigationItemAuthorization.cs:line
38
By debugging through the code, I found it internally calls System.Web.MVC classes to retrieve authorization information for the node. Below is the line that returns null for invalid models otherwise it works fine if its a redirect or normal GET operation.
ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);
if (actionDescriptor == null)
{
return null; **//it returns null for View(model)**
}
return new AuthorizationContext(controllerContext, actionDescriptor) { Controller = { ControllerContext = controllerContext } };
FindAction method in System.Web.Mvc. RunSelectionFilters is getting called from this method which is producing different outputs for normal and exception cases.
public MethodInfo FindActionMethod(ControllerContext controllerContext, string actionName)
{
List<MethodInfo> matchingAliasedMethods = this.GetMatchingAliasedMethods(controllerContext, actionName);
matchingAliasedMethods.AddRange(this.NonAliasedMethods[actionName]);
List<MethodInfo> ambiguousMethods = RunSelectionFilters(controllerContext, matchingAliasedMethods);
switch (ambiguousMethods.Count)
{
case 0:
return null;
case 1:
return ambiguousMethods[0];
}
throw this.CreateAmbiguousMatchException(ambiguousMethods, actionName);
}
RunSelectionFilters method is returning list2 for normal operations and list for exception cases.
private static List<MethodInfo> RunSelectionFilters(ControllerContext controllerContext, List<MethodInfo> methodInfos)
{
List<MethodInfo> list = new List<MethodInfo>();
List<MethodInfo> list2 = new List<MethodInfo>();
using (List<MethodInfo>.Enumerator enumerator = methodInfos.GetEnumerator())
{
Func<ActionMethodSelectorAttribute, bool> predicate = null;
MethodInfo methodInfo;
while (enumerator.MoveNext())
{
methodInfo = enumerator.Current;
ICollection<ActionMethodSelectorAttribute> actionMethodSelectorAttributes = ReflectedAttributeCache.GetActionMethodSelectorAttributes(methodInfo);
if (actionMethodSelectorAttributes.Count == 0)
{
list2.Add(methodInfo);
}
else
{
if (predicate == null)
{
predicate = attr => attr.IsValidForRequest(controllerContext, methodInfo);
}
if (actionMethodSelectorAttributes.All<ActionMethodSelectorAttribute>(predicate))
{
list.Add(methodInfo);
}
}
}
}
if (list.Count <= 0)
{
return list2;
}
return list;
}
And below is the custom authorization attribute. This attribute is applied to BaseController.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AppActionAuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
protected virtual bool AuthorizeCore(HttpContextBase httpContext)
{
return DBService.IsAuthorized(httpContext.RequestContext().RouteData.Values);
}
public virtual void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (OutputCacheAttribute.IsChildActionCacheActive(filterContext))
{
throw new InvalidOperationException("AuthorizeAttribute cannot be used within a child action caching block.");
}
if (this.AuthorizeCore(filterContext.HttpContext))
{
HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0L));
cache.AddValidationCallback(new HttpCacheValidateHandler(this.CacheValidateHandler), null);
}
else
{
this.HandleUnauthorizedRequest(filterContext);
}
}
private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
{
validationStatus = this.OnCacheAuthorization(new HttpContextWrapper(context));
}
protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
if (!this.AuthorizeCore(httpContext))
{
return HttpValidationStatus.IgnoreThisRequest;
}
return HttpValidationStatus.Valid;
}
}
I made custom Validator attribute
partial class DataTypeInt : ValidationAttribute
{
public DataTypeInt(string resourceName)
{
base.ErrorMessageResourceType = typeof(blueddPES.Resources.PES.Resource);
base.ErrorMessageResourceName = resourceName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
string number = value.ToString().Trim();
int val;
bool result = int.TryParse(number,out val );
if (result)
{
return ValidationResult.Success;
}
else
{
return new ValidationResult("");
}
}
}
But when entered string instead of int value in my textbox then value==null and when i entered int value then value==entered value;. Why?
Is there any alternate by which i can achieve the same (make sure at server side only)
The reason this happens is because the model binder (which runs before any validators) is unable to bind an invalid value to integer. That's why inside your validator you don't get any value. If you want to be able to validate this you could write a custom model binder for the integer type.
Here's how such model binder could look like:
public class IntegerBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
int temp;
if (value == null ||
string.IsNullOrEmpty(value.AttemptedValue) ||
!int.TryParse(value.AttemptedValue, out temp)
)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "invalid integer");
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
return null;
}
return temp;
}
}
and you will register it in Application_Start:
ModelBinders.Binders.Add(typeof(int), new IntegerBinder());
But you might ask: what if I wanted to customize the error message? After all, that's what I was trying to achieve in the first place. What's the point of writing this model binder when the default one already does that for me, it's just that I am unable to customize the error message?
Well, that's pretty easy. You could create a custom attribute which will be used to decorate your view model with and which will contain the error message and inside the model binder you will be able to fetch this error message and use it instead.
So, you could have a dummy validator attribute:
public class MustBeAValidInteger : ValidationAttribute, IMetadataAware
{
public override bool IsValid(object value)
{
return true;
}
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.AdditionalValues["errorMessage"] = ErrorMessage;
}
}
that you could use to decorate your view model:
[MustBeAValidInteger(ErrorMessage = "The value {0} is not a valid quantity")]
public int Quantity { get; set; }
and adapt the model binder:
public class IntegerBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
int temp;
var attemptedValue = value != null ? value.AttemptedValue : string.Empty;
if (!int.TryParse(attemptedValue, out temp)
)
{
var errorMessage = "{0} is an invalid integer";
if (bindingContext.ModelMetadata.AdditionalValues.ContainsKey("errorMessage"))
{
errorMessage = bindingContext.ModelMetadata.AdditionalValues["errorMessage"] as string;
}
errorMessage = string.Format(errorMessage, attemptedValue);
bindingContext.ModelState.AddModelError(bindingContext.ModelName, errorMessage);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
return null;
}
return temp;
}
}
I made a test website to debug an issue I'm having, and it appears that either I'm passing in the JSON data wrong or MVC just can't bind nullable longs. I'm using the latest MVC 3 release, of course.
public class GetDataModel
{
public string TestString { get; set; }
public long? TestLong { get; set; }
public int? TestInt { get; set; }
}
[HttpPost]
public ActionResult GetData(GetDataModel model)
{
// Do stuff
}
I'm posting a JSON string with the correct JSON content type:
{ "TestString":"test", "TestLong":12345, "TestInt":123 }
The long isn't bound, it's always null. It works if I put the value in quotes, but I shouldn't have to do that, should I? Do I need to have a custom model binder for that value?
I created a testproject just to test this. I put your code into my HomeController and added this to index.cshtml:
<script type="text/javascript">
$(function () {
$.post('Home/GetData', { "TestString": "test", "TestLong": 12345, "TestInt": 123 });
});
</script>
I put a breakpoint in the GetData method, and the values were binded to the model like they should:
So I think there's something wrong with the way you send the values. Are you sure the "TestLong" value is actually sent over the wire? You can check this using Fiddler.
If you don't want to go with Regex and you only care about fixing long?, the following will also fix the problem:
public class JsonModelBinder : DefaultModelBinder {
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
var propertyType = propertyDescriptor.PropertyType;
if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
var provider = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (provider != null
&& provider.RawValue != null
&& Type.GetTypeCode(provider.RawValue.GetType()) == TypeCode.Int32)
{
var value = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize(provider.AttemptedValue, bindingContext.ModelMetadata.ModelType);
return value;
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
My colleague came up with a workaround for this. The solution is to take the input stream and use a Regex to wrap all numeric variables in quotes to trick the JavaScriptSerializer into deserialising the longs properly. It's not a perfect solution, but it takes care of the issue.
This is done in a custom model binder. I used Posting JSON Data to ASP.NET MVC as an example. You have to take care, though, if the input stream is accessed anywhere else.
public class JsonModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (!IsJSONRequest(controllerContext))
return base.BindModel(controllerContext, bindingContext);
// Get the JSON data that's been posted
var jsonStringData = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();
// Wrap numerics
jsonStringData = Regex.Replace(jsonStringData, #"(?<=:)\s{0,4}(?<num>[\d\.]+)\s{0,4}(?=[,|\]|\}]+)", "\"${num}\"");
// Use the built-in serializer to do the work for us
return new JavaScriptSerializer().Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);
}
private static bool IsJSONRequest(ControllerContext controllerContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
return contentType.Contains("application/json");
}
}
Then put this in the Global:
ModelBinders.Binders.DefaultBinder = new JsonModelBinder();
Now the long gets bound successfully. I would call this a bug in the JavaScriptSerializer. Also note that arrays of longs or nullable longs get bound just fine without the quotes.
You can use this model binder class
public class LongModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (string.IsNullOrEmpty(valueResult.AttemptedValue))
{
return (long?)null;
}
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
actualValue = Convert.ToInt64(
valueResult.AttemptedValue,
CultureInfo.InvariantCulture
);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
In Global.asax Application_Start add these lines
ModelBinders.Binders.Add(typeof(long), new LongModelBinder());
ModelBinders.Binders.Add(typeof(long?), new LongModelBinder());
I wanted to incorporate the solution presented by Edgar but still have the features of the DefaultModelBinder. So instead of creating a new model binder I went with a different approach and replaced the JsonValueProviderFactory with a custom one. There's only a minor change in the code from the original MVC3 source code:
public sealed class NumericJsonValueProviderFactory : ValueProviderFactory
{
private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
{
IDictionary<string, object> d = value as IDictionary<string, object>;
if (d != null)
{
foreach (KeyValuePair<string, object> entry in d)
{
AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
}
return;
}
IList l = value as IList;
if (l != null)
{
for (int i = 0; i < l.Count; i++)
{
AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
}
return;
}
// primitive
backingStore[prefix] = value;
}
private static object GetDeserializedObject(ControllerContext controllerContext)
{
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
{
// not JSON request
return null;
}
StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
string bodyText = reader.ReadToEnd();
if (String.IsNullOrEmpty(bodyText))
{
// no JSON data
return null;
}
JavaScriptSerializer serializer = new JavaScriptSerializer();
// below is the code that Edgar proposed and the only change to original source code
bodyText = Regex.Replace(bodyText, #"(?<=:)\s{0,4}(?<num>[\d\.]+)\s{0,4}(?=[,|\]|\}]+)", "\"${num}\"");
object jsonData = serializer.DeserializeObject(bodyText);
return jsonData;
}
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
object jsonData = GetDeserializedObject(controllerContext);
if (jsonData == null)
{
return null;
}
Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
AddToBackingStore(backingStore, String.Empty, jsonData);
return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
}
private static string MakeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
}
private static string MakePropertyKey(string prefix, string propertyName)
{
return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
}
}
Then to register the new value provider you need to add the following lines to your Global.asax:
ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new NumericJsonValueProviderFactory());
Good evening,
I'm having trouble with model binding and validation but I don't know whether it's a normal behavior : the problem is that, is spite of BindAttribute (with his property Excluded correctly filled), the excluded properties are validated but not removed in the ModelState dictionary... so I get errors in my views... concerning an excluded property! Doh!
So, is there a way to get the "non-excluded-properties" list, directly in my model validator so I can tell my validation service not to validate excluded properties?
Here are the validator provider and the validator itself (just an internal wrapper around the great FluentValidator)
internal sealed class ValidationProvider : ModelValidatorProvider {
private readonly IValidationFactory _validationFactory;
public ValidationProvider(IValidationFactory validationFactory) {
_validationFactory = validationFactory;
}
public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) {
if (metadata.ModelType != null) {
IValidationService validationService;
if (_validationFactory.TryCreateServiceFor(metadata.ModelType, out validationService)) {
yield return new ValidationAdapter(metadata, context, validationService);
}
}
}
private sealed class ValidationAdapter : ModelValidator {
private readonly IValidationService _validationService;
internal ValidationAdapter(ModelMetadata metadata,
ControllerContext controllerContext,
IValidationService validationService)
: base(metadata, controllerContext) {
_validationService = validationService;
}
public override IEnumerable<ModelValidationResult> Validate(object container) {
if (Metadata.Model != null) {
IEnumerable<ValidationFault> validationFaults;
if (!_validationService.TryValidate(Metadata.Model, out validationFaults)) {
return validationFaults.Select(fault => new ModelValidationResult {
MemberName = fault.PropertyInfo.Name,
Message = fault.FaultedRule.Message
});
}
}
return Enumerable.Empty<ModelValidationResult>();
}
}
}
And here is the action :
public class MyModel {
public string Test { get; set; }
public string Name { get; set; }
}
[HttpPost]
public ActionResult Test([Bind(Exclude = "Test")] MyModel model) {
if (ModelState.IsValid) {
...
}
return View();
}
Here, I get errors for excluded "Test" property... Huh!
Thanks!
This is the expected behavior. This change (always doing whole-model validation) was made late in the MVC 2 ship cycle based on customer feedback (and the principle of least surprise).
More information:
http://bradwilson.typepad.com/blog/2010/01/input-validation-vs-model-validation-in-aspnet-mvc.html
For those who want avoiding the "validate everything then delete unwanted properties" scenario, I've extended the default model binder using a nested model metadata provider (because the "Properties" property of ModelMetadata is readonly...) :
So, now, I can only validate "non-excluded properties" :
public class OldWayValidationBinder : DefaultModelBinder {
private readonly ModelMetadataProvider _metadataProvider;
public ValidationBinder(ModelMetadataProvider metadataProvider) {
_metadataProvider = metadataProvider;
}
protected ModelMetadata CreateModelMetadata(ModelBindingContext bindingContext) {
var metadataProvider = new ModelMetadataProviderAdapter(
_metadataProvider, bindingContext.PropertyFilter);
return new ModelMetadata(metadataProvider,
bindingContext.ModelMetadata.ContainerType,
() => bindingContext.ModelMetadata.Model,
bindingContext.ModelMetadata.ModelType,
bindingContext.ModelMetadata.PropertyName);
}
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) {
base.OnModelUpdated(controllerContext, new ModelBindingContext(bindingContext) {
ModelMetadata = CreateModelMetadata(bindingContext)
});
}
private sealed class ModelMetadataProviderAdapter : ModelMetadataProvider {
private readonly ModelMetadataProvider _innerMetadataProvider;
private readonly Predicate<string> _propertyFilter;
internal ModelMetadataProviderAdapter(
ModelMetadataProvider innerMetadataProvider,
Predicate<string> propertyFilter) {
_innerMetadataProvider = innerMetadataProvider;
_propertyFilter = propertyFilter;
}
public override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType) {
return _innerMetadataProvider.GetMetadataForProperties(container, containerType)
.Where(metadata => _propertyFilter(metadata.PropertyName));
}
public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName) {
return _innerMetadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName);
}
public override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType) {
return _innerMetadataProvider.GetMetadataForType(modelAccessor, modelType);
}
}
}
internal sealed class ValidationProvider : ModelValidatorProvider {
private readonly IValidationFactory _validationFactory;
public ValidationProvider(IValidationFactory validationFactory) {
_validationFactory = validationFactory;
}
public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) {
if (metadata.ModelType != null) {
IValidationService validationService;
if (_validationFactory.TryCreateServiceFor(metadata.ModelType, out validationService)) {
yield return new ModelValidatorAdapter(metadata, context, validationService);
}
}
}
private sealed class ModelValidatorAdapter : ModelValidator {
private readonly IValidationService _validationService;
internal ValidationAdapter(ModelMetadata metadata,
ControllerContext controllerContext,
IValidationService validationService)
: base(metadata, controllerContext) {
_validationService = validationService;
}
public override IEnumerable<ModelValidationResult> Validate(object container) {
if (Metadata.Model != null) {
IEnumerable<ValidationFault> validationFaults;
var validatableProperties = Metadata.Properties.Select(metadata => Metadata.ModelType.GetProperty(metadata.PropertyName));
if (!_validationService.TryValidate(Metadata.Model, validatableProperties, out validationFaults)) {
return validationFaults.Select(fault => new ModelValidationResult {
MemberName = fault.PropertyInfo.Name,
Message = fault.FaultedRule.Message
});
}
}
return Enumerable.Empty<ModelValidationResult>();
}
}
}
Nonetheless, I believe this scenario must be present as an option in MVC. At least, the unbound properties list should be given as a parameter of ModelValidatorProvider's GetValidators method!
I think the "old behavior" can be easily recovered by overriding the OnModelUpdating method of the DefaultModelBinder. Please point me in the right direction whether it's not the good way to achieve that :
internal sealed class OldWayModelBinder : DefaultModelBinder {
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) {
foreach (var validationResult in ModelValidator.GetModelValidator(bindingContext.ModelMetadata, controllerContext).Validate(null)) {
string subPropertyName = CreateSubPropertyName(bindingContext.ModelName, validationResult.MemberName);
if (bindingContext.PropertyFilter(subPropertyName)) {
if (bindingContext.ModelState.IsValidField(subPropertyName)) {
bindingContext.ModelState.AddModelError(subPropertyName, validationResult.Message);
}
}
}
}
}
(however, the fact that the IsValidField method returns true if a given property is faulted is a little bit strange or there is something I don't understand!)