Server side validation of int datatype - c#

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

Related

DataAnnotations for decimal as a percentage

I'm using the following DataAnnotations for a field on a data object in a c# .netcore razor pages application:
[Display(Name = "Markup For Profit")]
[Required]
[DisplayFormat(DataFormatString = "{0:P0}", ApplyFormatInEditMode = true)]
public double ProfitMarkup { get; set; }
On the Razor page I use this field in an input:
<input asp-for="ProfitMarkup" class="form-control" autocomplete="off" />
A decimal value of ProfitMarkup displays correctly (i.e. 0.2 displays as 20%) which is good. However when I go to submit the form the client side validation raises the error:
"The field Markup For Profit must be a number."
Which I'm pretty sure is because the '%' sign has been added by the display format.
What is the best way to allow an input on the page of 20% to make it through to the object and set set the decimal to 0.2 ?
Adding a new Percentage type and a PercentageConverter TypeConverter has solved this problem for me.
I have declared a new property that reads the double ProfitMarkup property like this:
[Display(Name = "Markup For Profit")]
[Required]
[NotMapped]
public Percentage ProfitMarkupPercentage { get { return new Percentage(ProfitMarkup); } set { ProfitMarkup = value; } }
ProfitMarkup gets written to/from the database as a double and ProfitMarkupPercentage is displayed on the razor page like this:
<input asp-for="ProfitMarkupPercentage" class="form-control" autocomplete="off" />
The code for percentage object and it's TypeConverter is (this was provided kindly by the response in this thread: How to convert percentage string to double?):
[TypeConverter(typeof(PercentageConverter))]
public struct Percentage
{
public double Value;
public Percentage(double value)
{
Value = value;
}
static public implicit operator double(Percentage pct)
{
return pct.Value;
}
static public implicit operator string(Percentage pct) { return pct.ToString(); }
public Percentage(string value)
{
Value = 0.0;
var pct = (Percentage)TypeDescriptor.GetConverter(GetType()).ConvertFromString(value);
Value = pct.Value;
}
public override string ToString()
{
return ToString(CultureInfo.InvariantCulture);
}
public string ToString(CultureInfo Culture)
{
return TypeDescriptor.GetConverter(GetType()).ConvertToString(null, Culture, this);
}
}
public class PercentageConverter : TypeConverter
{
static TypeConverter conv = TypeDescriptor.GetConverter(typeof(double));
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return conv.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(Percentage))
{
return true;
}
return conv.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (value == null)
{
return new Percentage();
}
if (value is string)
{
string s = value as string;
s = s.TrimEnd(' ', '\t', '\r', '\n');
var percentage = s.EndsWith(culture.NumberFormat.PercentSymbol);
if (percentage)
{
s = s.Substring(0, s.Length - culture.NumberFormat.PercentSymbol.Length);
}
double result = (double)conv.ConvertFromString(s);
if (percentage)
{
result /= 100;
}
return new Percentage(result);
}
return new Percentage((double)conv.ConvertFrom(context, culture, value));
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (!(value is Percentage))
{
throw new ArgumentNullException("value");
}
var pct = (Percentage)value;
if (destinationType == typeof(string))
{
return conv.ConvertTo(context, culture, pct.Value * 100, destinationType) + culture.NumberFormat.PercentSymbol;
}
return conv.ConvertTo(context, culture, pct.Value, destinationType);
}
}
The default model binder now populates to and from the property on to the razor page correctly.
You can also add validation data annotation to the ProfitMarkupPercentage property using the Regex:
[RegularExpression(#"^(0*100{1,1}\.?((?<=\.)0*)?%?$)|(^0*\d{0,2}\.?((?<=\.)\d*)?%?)$", ErrorMessage = "Invalid percentage")]
The default ModelBinder won't be able to get the correct posted value , when you have the Percent sign , in the input box, as part of DataAnnotation. You may create a custom ModelBinder. An example using ASP.Net MVC:
// The ViewModel
public class HomeViewModel
{
[Display(Name = "Markup For Profit")]
[Required]
[DisplayFormat(DataFormatString = "{0:P2}", ApplyFormatInEditMode =true)]
public double ProfitMarkup { get; set; }
}
// Two Classes to implement custom ModelBinder
public class DoublePercentDataBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(HomeViewModel))
{
HttpRequestBase request = controllerContext.HttpContext.Request;
string pMarkup = request.Form.Get("ProfitMarkup").TrimEnd('%');
return new HomeViewModel
{
ProfitMarkup = Double.Parse(pMarkup)/100
};
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
}
public class DoublePercentBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
string pMarkup = request.Form.Get("ProfitMarkup").TrimEnd('%');
return new HomeViewModel
{
ProfitMarkup = Double.Parse(pMarkup)/100
};
}
}
// Attach it in Application_Start in Global.asax
protected void Application_Start()
{
ModelBinders.Binders.Add(typeof(HomeViewModel), new DoublePercentBinder());
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
// HomeController Get and Post
public class HomeController : Controller
{
public ActionResult Index()
{
HomeViewModel vm = new HomeViewModel();
vm.ProfitMarkup = 1.2;
return View(vm);
}
[HttpPost]
public ActionResult Index([ModelBinder(typeof(DoublePercentBinder))] HomeViewModel hvm)
{
return View(hvm);
}
// The Razor View
#using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
<div>
#Html.EditorFor(x => x.ProfitMarkup)
#Html.ValidationMessageFor(x => x.ProfitMarkup)
</div>
<br />
<input type="submit" value="Submit" />
}

Custom Validation Attribute reading from a form field

I am trying to create my own validation attribute IsUnique that checks existing values for given property. I understand IsValid() must be overridden so that custom validation attribute can work. So far I have seen examples with validate attributes that take a string parameters which is then compared with hard coded values inside IsValid() method.
I need IsValid() method to get access to a property and its value to further compare it with values in the database.
This is what I have done so far:
public class IsUnique : ValidationAttribute
{
private string codeno { get; set; }
: base("{0} is already in use")
public IsUnique (string codeno)
{
this.codeno = codeno;
}
public override ValidationResult IsValid(object value,
ValidationContext vContext)
{
if (value != null)
{
MyDBContext db = new MyDBContext();
Student studentsCodeNo =
db.Students.FirstOrDefault(r => r.codeno== (string)value);
if (studentsCodeNo != null)
{
string errorMessage =
FormatErrorMessage(vContext.DisplayName);
return new ValidationResult(errorMessage);
}
}
return ValidationResult.Success;
}
}
As said, the problem is that this version takes parameter. I would like codeno to be read from a user form field, and such value would then be compared against anything in database. I don't know how to read values from the form fields.
Here is code
public class IsUnique : ValidationAttribute{
public override ValidationResult IsValid(object value,
ValidationContext vContext)
{
PropertyInfo property = validationContext.ObjectType.GetProperty("Codeno");
if (property == null)
return new ValidationResult(string.Format("Property '{0}' is undefined","Codeno"));
var fieldValue = property.GetValue(validationContext.ObjectInstance, null);
string codeno= (fieldValue == null ? "" : fieldValue.ToString());
if (!string.IsNullOrEmpty(codeno))
{
MyDBContext db = new MyDBContext();
Student studentsCodeNo =
db.Students.FirstOrDefault(r => r.codeno== codeno);
if (studentsCodeNo != null)
{
string errorMessage =
FormatErrorMessage(vContext.DisplayName);
return new ValidationResult(errorMessage);
}
}
return ValidationResult.Success; }}
There is sort of an out of the box way to do this already
http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.schema.indexattribute(v=vs.113).aspx
[Index(IsUnique=true)]

Required Attribute for DateTime property

I've wrote the below code for making appointed date as required field. But when remove the default date and try to submit, no error message is shown.
[DisplayName("Appointed Date")]
[Required(ErrorMessage = "Appointed Date Is Required")]
public virtual DateTime AppointedDate { get; set; }
Please let me know, if i need to do anything more.
Usually this has to do with non-nullable types failing in the model binder on parsing. Make the date nullable in the model and see if that solves your problem. Otherwise, write your own model binder and handle this better.
Edit: And by model I mean a view model for the view, to make the recommended change, if you want to stick with binding to your model in the view (which I am assuming is using EF), follow the write your own model binder suggestion
Edit 2: We did something like this to get a custom format to parse in a nullable datetime (which might be a good start for you to tweak for a non-nullable type):
public sealed class DateTimeBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
if (valueProviderResult == null)
{
return null;
}
var attemptedValue = valueProviderResult.AttemptedValue;
return ParseDateTimeInfo(bindingContext, attemptedValue);
}
private static DateTime? ParseDateTimeInfo(ModelBindingContext bindingContext, string attemptedValue)
{
if (string.IsNullOrEmpty(attemptedValue))
{
return null;
}
if (!Regex.IsMatch(attemptedValue, #"^\d{2}-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{4}$", RegexOptions.IgnoreCase))
{
var displayName = bindingContext.ModelMetadata.DisplayName;
var errorMessage = string.Format("{0} must be in the format DD-MMM-YYYY", displayName);
bindingContext.ModelState.AddModelError(bindingContext.ModelMetadata.PropertyName, errorMessage);
return null;
}
return DateTime.Parse(attemptedValue);
}
}
Then register this with (by providing this class in your Dependency Injection container):
public class EventOrganizerProviders : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
if (modelType == typeof(DateTime))
{
return new DateTimeBinder();
}
// Other types follow
if (modelType == typeof(TimeSpan?))
{
return new TimeSpanBinder();
}
return null;
}
}

MVC 3 doesn't bind nullable long

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());

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