Excluded properties via BindAttribute and ModelValidator in ASP.NET MVC 3 - c#

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!)

Related

How to access custom method attributes from ProxyGenerationHook in Castle.DynamicProxy

I'm implementing Interceptor mechanism in .NET with Castle.DynamicProxy (Castle.Core 4.4.0). I'm following this tutorial for selecting which method to intercept: https://kozmic.net/2009/01/17/castle-dynamic-proxy-tutorial-part-iii-selecting-which-methods-to/
It was given an example in this article about "selecting which methods to intercept":
public class FreezableProxyGenerationHook:IProxyGenerationHook
{
public bool ShouldInterceptMethod(Type type, MethodInfo memberInfo)
{
return !memberInfo.Name.StartsWith("get_", StringComparison.Ordinal);
}
//implementing other methods...
}
According to this article, I implemented the ShouldInterceptMethod like below but i can not access the method's custom attributes.
public class ProductServiceProxyGenerationHook : IProxyGenerationHook
{
public void MethodsInspected()
{
}
public void NonProxyableMemberNotification(Type type, MemberInfo memberInfo)
{
}
public bool ShouldInterceptMethod(Type type, MethodInfo methodInfo)
{
//return methodInfo.CustomAttributes.Any(a => a.GetType() == typeof(UseInterceptorAttribute));
return methodInfo.CustomAttributes.Count() > 0;
}
}
This is the method that i want to intercept:
[UseInterceptor]
public Product AOP_Get(string serialNumber)
{
throw new NotImplementedException();
}
This is my custom attribute:
[System.AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
sealed class UseInterceptorAttribute : Attribute
{
public UseInterceptorAttribute()
{
}
}
When ShouldInterceptMethod invoked for AOP_Get method, there aren't any custom attributes on local variable methodInfo. As a result ShouldInterceptMethod returns false. But when i check from the AOP_Get method body, i can access custom attribute like below:
How can i access custom attributes in ShouldInterceptMethod method?
I solved the problem using UseInterceptorAttribute in the interface methods. Because the interceptor is enabling for interface:
//autofac
var proxyGenerationOptions = new ProxyGenerationOptions(new ProductServiceProxyGenerationHook());
builder.RegisterType<ProductService>()
.As<IProductService>()
.EnableInterfaceInterceptors(proxyGenerationOptions)
.InterceptedBy(typeof(LoggingInterceptor));
The solution:
public interface IProductService
{
Product Get(string productNumber, string serialNumber);
bool Create(string productNumber, string serialNumber);
[UseInterceptor]
Product AOP_Get(string productNumber, string serialNumber);
}
public class ProductService : IProductService
{
public Product AOP_Get(string productNumber, string serialNumber)
{
var m = GetType().GetMethod("AOP_Get");
return null;
}
public bool Create(string productNumber, string serialNumber)
{
return true;
}
public Product Get(string productNumber, string serialNumber)
{
return new Product
{
Id = 5,
ProductNumber = "testPN",
SerialNumber = "testSN"
};
}
}
public class ProductServiceProxyGenerationHook : IProxyGenerationHook
{
public void MethodsInspected()
{
}
public void NonProxyableMemberNotification(Type type, MemberInfo memberInfo)
{
}
public bool ShouldInterceptMethod(Type type, MethodInfo methodInfo)
{
return methodInfo
.CustomAttributes
.Any(a => a.AttributeType == typeof(UseInterceptorAttribute));
}
}
}

Using default IModelBinder within custom binder in Web API 2

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;
}
}
}

Server side validation of int datatype

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;
}
}

C# How to expose a property based on the value of another property in a propertygrid?

I have class with multiple properties. Sometimes a property (A) may be edited in a propertygrid. But sometimes property A may not be edited. This depends on a value of another property.
How can I do this?
EDIT:
I am sorry, I forget to mention that I want this in design-time.
Runtime property models are an advanced topic. For PropertyGrid the easiest route would be to write a TypeConverter, inheriting from ExpandableObjectConverter. Override GetProperties, and swap the property in question for a custom one.
Writing a PropertyDescriptor from scratch is a chore; but in this case you mainly just need to chain ("decorator") all the methods to the original (reflective) descriptor. And just override IsReadOnly to return the bool you want.
By no means trivial, but achievable.
using System;
using System.ComponentModel;
using System.Windows.Forms;
static class Program
{
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.Run(new Form { Text = "read only",
Controls = {
new PropertyGrid { Dock = DockStyle.Fill, SelectedObject = new Foo { IsBarEditable = false }}
}
});
Application.Run(new Form { Text = "read write",
Controls = {
new PropertyGrid { Dock = DockStyle.Fill, SelectedObject = new Foo { IsBarEditable = true }}
}
});
}
}
[TypeConverter(typeof(Foo.FooConverter))]
class Foo
{
[Browsable(false)]
public bool IsBarEditable { get; set; }
public string Bar { get; set; }
private class FooConverter : ExpandableObjectConverter
{
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
var props = base.GetProperties(context, value, attributes);
if (!((Foo)value).IsBarEditable)
{ // swap it
PropertyDescriptor[] arr = new PropertyDescriptor[props.Count];
props.CopyTo(arr, 0);
for (int i = 0; i < arr.Length; i++)
{
if (arr[i].Name == "Bar") arr[i] = new ReadOnlyPropertyDescriptor(arr[i]);
}
props = new PropertyDescriptorCollection(arr);
}
return props;
}
}
}
class ReadOnlyPropertyDescriptor : ChainedPropertyDescriptor
{
public ReadOnlyPropertyDescriptor(PropertyDescriptor tail) : base(tail) { }
public override bool IsReadOnly
{
get
{
return true;
}
}
public override void SetValue(object component, object value)
{
throw new InvalidOperationException();
}
}
abstract class ChainedPropertyDescriptor : PropertyDescriptor
{
private readonly PropertyDescriptor tail;
protected PropertyDescriptor Tail { get {return tail; } }
public ChainedPropertyDescriptor(PropertyDescriptor tail) : base(tail)
{
if (tail == null) throw new ArgumentNullException("tail");
this.tail = tail;
}
public override void AddValueChanged(object component, System.EventHandler handler)
{
tail.AddValueChanged(component, handler);
}
public override AttributeCollection Attributes
{
get
{
return tail.Attributes;
}
}
public override bool CanResetValue(object component)
{
return tail.CanResetValue(component);
}
public override string Category
{
get
{
return tail.Category;
}
}
public override Type ComponentType
{
get { return tail.ComponentType; }
}
public override TypeConverter Converter
{
get
{
return tail.Converter;
}
}
public override string Description
{
get
{
return tail.Description;
}
}
public override bool DesignTimeOnly
{
get
{
return tail.DesignTimeOnly;
}
}
public override string DisplayName
{
get
{
return tail.DisplayName;
}
}
public override PropertyDescriptorCollection GetChildProperties(object instance, Attribute[] filter)
{
return tail.GetChildProperties(instance, filter);
}
public override object GetEditor(Type editorBaseType)
{
return tail.GetEditor(editorBaseType);
}
public override object GetValue(object component)
{
return tail.GetValue(component);
}
public override bool IsBrowsable
{
get
{
return tail.IsBrowsable;
}
}
public override bool IsLocalizable
{
get
{
return tail.IsLocalizable;
}
}
public override bool IsReadOnly
{
get { return tail.IsReadOnly; }
}
public override string Name
{
get
{
return tail.Name;
}
}
public override Type PropertyType
{
get { return tail.PropertyType; }
}
public override void RemoveValueChanged(object component, EventHandler handler)
{
tail.RemoveValueChanged(component, handler);
}
public override void ResetValue(object component)
{
tail.ResetValue(component);
}
public override void SetValue(object component, object value)
{
tail.SetValue(component, value);
}
public override bool ShouldSerializeValue(object component)
{
return tail.ShouldSerializeValue(component);
}
public override bool SupportsChangeEvents
{
get
{
return tail.SupportsChangeEvents;
}
}
}
This answer assumes you are talking about WinForms. If you would like to change one property's readonly state based on another, you will need to have your object implement ICustomTypeDescriptor. This isn't a simple thing to do, but it will give you lots of flexibility about how your class is displayed in the propertygrid.
I've offered a similar solution in the past via this stack solution. It makes use of a custom property, and conditionally ignores an attempt to change at design-time vs run-time, but I'm sure could be altered in the SETter by applying your own "criteria" to allow it being changed...

ASP.NET MVC 2 Beta - Default Model Binder

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?

Categories