I have a custom model class which contains a decimal member and a view to accept entry for this class. Everything worked well till I added javascripts to format the number inside input control. The format code format the inputted number with thousand separator ',' when focus blur.
The problem is that the decimal value inside my modal class isn't bind/parsed well with thousand separator. ModelState.IsValid returns false when I tested it with "1,000.00" but it is valid for "100.00" without any changes.
Could you share with me if you have any solution for this?
Thanks in advance.
Sample Class
public class Employee
{
public string Name { get; set; }
public decimal Salary { get; set; }
}
Sample Controller
public class EmployeeController : Controller
{
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult New()
{
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult New(Employee e)
{
if (ModelState.IsValid) // <-- It is retruning false for values with ','
{
//Subsequence codes if entry is valid.
//
}
return View(e);
}
}
Sample View
<% using (Html.BeginForm())
{ %>
Name: <%= Html.TextBox("Name")%><br />
Salary: <%= Html.TextBox("Salary")%><br />
<button type="submit">Save</button>
<% } %>
I tried a workaround with Custom ModelBinder as Alexander suggested. The probelm solved. But the solution doesn't go well with IDataErrorInfo implementation. The Salary value become null when 0 is entered because of the validation. Any suggestion, please?
Do Asp.Net MVC team members come to stackoverflow? Can I get a little help from you?
Updated Code with Custom Model Binder as Alexander suggested
Model Binder
public class MyModelBinder : DefaultModelBinder {
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
if (bindingContext == null) {
throw new ArgumentNullException("bindingContext");
}
ValueProviderResult valueResult;
bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out valueResult);
if (valueResult != null) {
if (bindingContext.ModelType == typeof(decimal)) {
decimal decimalAttempt;
decimalAttempt = Convert.ToDecimal(valueResult.AttemptedValue);
return decimalAttempt;
}
}
return null;
}
}
Employee Class
public class Employee : IDataErrorInfo {
public string Name { get; set; }
public decimal Salary { get; set; }
#region IDataErrorInfo Members
public string this[string columnName] {
get {
switch (columnName)
{
case "Salary": if (Salary <= 0) return "Invalid salary amount."; break;
}
return string.Empty;
}
}
public string Error{
get {
return string.Empty;
}
}
#endregion
}
The reason behind it is, that in ConvertSimpleType in ValueProviderResult.cs a TypeConverter is used.
The TypeConverter for decimal does not support a thousand separator.
Read here about it: http://social.msdn.microsoft.com/forums/en-US/clr/thread/1c444dac-5d08-487d-9369-666d1b21706e
I did not check yet, but at that post they even said the CultureInfo passed into TypeConverter is not used. It will always be Invariant.
string decValue = "1,400.23";
TypeConverter converter = TypeDescriptor.GetConverter(typeof(decimal));
object convertedValue = converter.ConvertFrom(null /* context */, CultureInfo.InvariantCulture, decValue);
So I guess you have to use a workaround. Not nice...
I didn't like the solutions above and came up with this:
In my custom modelbinder, I basically replace the value with the culture invariant value if it is a decimal and then hand over the rest of the work to the default model binder.
The rawvalue being a array seems strange to me, but this is what I saw/stole in the original code.
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if(bindingContext.ModelType == typeof(decimal) || bindingContext.ModelType==typeof(Nullable<decimal>))
{
ValueProviderResult valueProviderResult = bindingContext.ValueProvider[bindingContext.ModelName];
if (valueProviderResult != null)
{
decimal result;
var array = valueProviderResult.RawValue as Array;
object value;
if (array != null && array.Length > 0)
{
value = array.GetValue(0);
if (decimal.TryParse(value.ToString(), out result))
{
string val = result.ToString(CultureInfo.InvariantCulture.NumberFormat);
array.SetValue(val, 0);
}
}
}
}
return base.BindModel(controllerContext, bindingContext);
}
It seems there are always workarounds of some form or another to be found in order to make the default model binder happy! I wonder if you could create a "pseudo" property that is used only by the model binder? (Note, this is by no means elegant. Myself, I seem to resort to similar tricks like this more and more often simply because they work and they get the job "done"...) Note also, if you were using a separate "ViewModel" (which I recommend for this), you could put this code in there, and leave your domain model nice and clean.
public class Employee
{
private decimal _Salary;
public string MvcSalary // yes, a string. Bind your form values to this!
{
get { return _Salary.ToString(); }
set
{
// (Using some pseudo-code here in this pseudo-property!)
if (AppearsToBeValidDecimal(value)) {
_Salary = StripCommas(value);
}
}
}
public decimal Salary
{
get { return _Salary; }
set { _Salary = value; }
}
}
P.S., after I typed this up, I look back at it now and am even hesitating to post this, it is so ugly! But if you think it might be helpful I'll let you decide...
Best of luck!
-Mike
I implement custom validator, adding validity of grouping.
The problem (that i solved in code below)is that parse method remove all thousands separator, so also 1,2,2 is considered valid.
Here my binder for decimal
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web.Mvc;
namespace EA.BUTruck.ContactCenter.Model.Extensions
{
public class DecimalModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
var trimmedvalue = valueResult.AttemptedValue.Trim();
actualValue = Decimal.Parse(trimmedvalue, CultureInfo.CurrentCulture);
string decimalSep = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
string thousandSep = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator;
thousandSep = Regex.Replace(thousandSep, #"\u00A0", " "); //used for culture with non breaking space thousand separator
if (trimmedvalue.IndexOf(thousandSep) >= 0)
{
//check validity of grouping thousand separator
//remove the "decimal" part if exists
string integerpart = trimmedvalue.Split(new string[] { decimalSep }, StringSplitOptions.None)[0];
//recovert double value (need to replace non breaking space with space present in some cultures)
string reconvertedvalue = Regex.Replace(((decimal)actualValue).ToString("N").Split(new string[] { decimalSep }, StringSplitOptions.None)[0], #"\u00A0", " ");
//if are the same, it is a valid number
if (integerpart == reconvertedvalue)
return actualValue;
//if not, could be differences only in the part before first thousand separator (for example original input stirng could be +1.000,00 (example of italian culture) that is valid but different from reconverted value that is 1.000,00; so we need to make a more accurate checking to verify if input string is valid
//check if number of thousands separators are the same
int nThousands = integerpart.Count(x => x == thousandSep[0]);
int nThousandsconverted = reconvertedvalue.Count(x => x == thousandSep[0]);
if (nThousands == nThousandsconverted)
{
//check if all group are of groupsize number characters (exclude the first, because could be more than 3 (because for example "+", or "0" before all the other numbers) but we checked number of separators == reconverted number separators
int[] groupsize = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSizes;
bool valid = ValidateNumberGroups(integerpart, thousandSep, groupsize);
if (!valid)
throw new FormatException();
}
else
throw new FormatException();
}
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
private bool ValidateNumberGroups(string value, string thousandSep, int[] groupsize)
{
string[] parts = value.Split(new string[] { thousandSep }, StringSplitOptions.None);
for (int i = parts.Length - 1; i > 0; i--)
{
string part = parts[i];
int length = part.Length;
if (groupsize.Contains(length) == false)
{
return false;
}
}
return true;
}
}
}
For decimal? nullable you need to add a little code before
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web.Mvc;
namespace EA.BUTruck.ContactCenter.Model.Extensions
{
public class DecimalNullableModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
//need this condition against non nullable decimal
if (string.IsNullOrWhiteSpace(valueResult.AttemptedValue))
return actualValue;
var trimmedvalue = valueResult.AttemptedValue.Trim();
actualValue = Decimal.Parse(trimmedvalue,CultureInfo.CurrentCulture);
string decimalSep = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
string thousandSep = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator;
thousandSep = Regex.Replace(thousandSep, #"\u00A0", " "); //used for culture with non breaking space thousand separator
if (trimmedvalue.IndexOf(thousandSep) >=0)
{
//check validity of grouping thousand separator
//remove the "decimal" part if exists
string integerpart = trimmedvalue.Split(new string[] { decimalSep }, StringSplitOptions.None)[0];
//recovert double value (need to replace non breaking space with space present in some cultures)
string reconvertedvalue = Regex.Replace(((decimal)actualValue).ToString("N").Split(new string[] { decimalSep }, StringSplitOptions.None)[0], #"\u00A0", " ");
//if are the same, it is a valid number
if (integerpart == reconvertedvalue)
return actualValue;
//if not, could be differences only in the part before first thousand separator (for example original input stirng could be +1.000,00 (example of italian culture) that is valid but different from reconverted value that is 1.000,00; so we need to make a more accurate checking to verify if input string is valid
//check if number of thousands separators are the same
int nThousands = integerpart.Count(x => x == thousandSep[0]);
int nThousandsconverted = reconvertedvalue.Count(x => x == thousandSep[0]);
if(nThousands == nThousandsconverted)
{
//check if all group are of groupsize number characters (exclude the first, because could be more than 3 (because for example "+", or "0" before all the other numbers) but we checked number of separators == reconverted number separators
int[] groupsize = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSizes;
bool valid = ValidateNumberGroups(integerpart, thousandSep, groupsize);
if (!valid)
throw new FormatException();
}
else
throw new FormatException();
}
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
private bool ValidateNumberGroups(string value, string thousandSep, int[] groupsize)
{
string[] parts = value.Split(new string[] { thousandSep }, StringSplitOptions.None);
for(int i = parts.Length-1; i > 0; i--)
{
string part = parts[i];
int length = part.Length;
if (groupsize.Contains(length) == false)
{
return false;
}
}
return true;
}
}
}
You need to create similar binder for double, double?, float, float? (the code is the same of DecimalModelBinder and DecimalNullableModelBinder; you need just to replace type in 2 point where there is "decimal").
Then in global.asax
ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
ModelBinders.Binders.Add(typeof(decimal?), new DecimalNullableModelBinder());
ModelBinders.Binders.Add(typeof(float), new FloatModelBinder());
ModelBinders.Binders.Add(typeof(float?), new FloatNullableModelBinder());
ModelBinders.Binders.Add(typeof(double), new DoubleModelBinder());
ModelBinders.Binders.Add(typeof(double?), new DoubleNullableModelBinder());
This solution works fine on server side, like the client part using jquery globalize and my fixing reported here
https://github.com/globalizejs/globalize/issues/73#issuecomment-275792643
Did you try to convert it to Decimal in the controller? This should do the trick:
string _val = "1,000.00";
Decimal _decVal = Convert.ToDecimal(_val);
Console.WriteLine(_decVal.ToString());
Hey I had one more thought... This builds on Naweed's answer, but will still let you use the default model binder. The concept is to intercept the posted form, modify some of the values in it, then pass the [modified] form collection to the UpdateModel (default model binder) method... I use a modified version of this for dealing with checkboxes/booleans, to avoid the situation where anything other than "true" or "false" causes an unhandled/silent exception within the model binder.
(You would of course want to refactor this to be more re-useable, to perhaps deal with all decimals)
public ActionResult myAction(NameValueCollection nvc)
{
Employee employee = new Employee();
string salary = nvc.Get("Salary");
if (AppearsToBeValidDecimal(salary)) {
nvc.Remove("Salary");
nvc.Add("Salary", StripCommas(salary));
}
if (TryUpdateModel(employee, nvc)) {
// ...
}
}
P.S., I may be confused on my NVC methods, but I think these will work.
Related
This is my first post.
I need string array validation such like below.
[Required(ErrorMessage = "Content name is required")]
public string[] ContentName { get; set; }
I found a post which has the same situation.
This answer and following code helped me so much and I could solve my problem.
public class StringArrayRequiredAttribute : ValidationAttribute
{
protected override ValidationResult IsValid (object value, ValidationContext validationContext)
{
string[] array = value as string[];
if(array == null || array.Any(item => string.IsNullOrEmpty(item)))
{
return new ValidationResult(this.ErrorMessage);
}
else
{
return ValidationResult.Success;
}
}
}
And
[StringArrayRequired(ErrorMessage = "Content name is required")]
public string[] ContentName { get; set; }
But now I found an another problem. This validation works only server side. I wish I could have a client validation too. Because it would make my client much happier!!
So would you give me a nice way for this? Waiting for your answers!!
Thank you for your help.
I write a short code in my view.
$.validator.addMethod('stringarrayrequired', function (value, element, params) {
let array = value;
if (array == null) {
return false;
}
for (var i = 0; i < array.length; i++) {
if (!array[i]) {
return false;
}
}
return true;
}, '');
$.validator.unobtrusive.adapters.add("stringarrayrequired", function (options) {
options.rules["stringarrayrequired"] = "#" + options.element.name.replace('.', '_'); // mvc html helpers
options.messages["stringarrayrequired"] = options.message;
});
(Sorry, I'm not fluent in JS...)
And I add id="stringarrayrequired" to my . But it doesn't work.
I also checked html code. When I click the submit button, there should be a class="input-validation-error" or "valid" in input tag for "ContentName", but I couldn't find both of them.
I still need more info... Anyone help?
I found a way to solve my problem.
(I changed property name ContextName to Selection)
[Display(Name = "Selections")]
public Selection[] Selections { get; set; }
public class Selection
{
[Required(ErrorMessage = "SelectionItem is empty")]
public string SelectionItem { get; set; }
}
I use Selections for , SelectionItem for and .
As you know, [Required] attribute doesn't work for string[]. So I created a Selection class and changed string[] to Selection[], and applied [Required] attribute to string.
I know that this's not a clean way... I'll use foolproof or something.
Add the following javaScript code in your view:
$.validator.addMethod('stringarrayrequired', function (value, element, params) {
// here return true or false based on checking the input value
},'');
$.validator.unobtrusive.adapters.add("stringarrayrequired", function (options) {
options.rules["stringarrayrequired"] = "#" + options.element.name.replace('.', '_'); // mvc html helpers
options.messages["stringarrayrequired"] = options.message;
});
I've got his code.
PartialView.
<div class="input width110">
#Html.EditorFor(x => x.Price, #Html.Attributes(#class: "right_text_align", #disabled: "true", #id: "Price"))
</div>
Model.
public class ServiceModel
{
[DisplayFormat(DataFormatString = "{0:0.00}", ApplyFormatInEditMode = true)]
public decimal Price { get; set; }
}
Controller
public ActionResult SetService(ServiceModel model, string action)
{
if (ModelState.IsValid)
{
/*Does smthg.*/
ModelState.Clear();
}
return View("Index", rcpModel);
//Index is main view, which holds partialView
//rcpModel holds, model
}
When view loads Decimal is displayed in format "0.00". But after post when modelState is invalid number in displayed in format "0.0000". If model state isvalid, everything goes well. Has anyone encountered anything similar?
If you have javascript modifying the values on textboxes (currency formatting or commas) then you might be getting binding errors because it will behave as a string. Try this:
Create a BindingProperty for decimal values
public class DecimalModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
actualValue = Convert.ToDecimal(valueResult.AttemptedValue,
CultureInfo.CurrentCulture);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
On your global.asax app_start or WebActivator.PostApplicationStartMethod add an entry to register the custom binder:
ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
To display dot instead of comma is enough to change the culture to english in every point of the code which is used before the view is called.
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("En");
I am experimenting with the following scenario.
on the initial GET, my controller is returning a default model with a string[] property.
on the view, I show this property using a textbox:
#Html.TextBoxFor(model => model.MyProperty)
The array is showing as comma delimited list. Great!
The problem is that when i postback, the list ends up as single string array with all items comma delimited within that string.
Is there a way I could provide a deserializer (maybe something equivalent of converter in WPF) that would make this go back to correct array?
I am aware that I can also use #Html.EditorFor(...), but this renders my array as a list of separate textboxes which I do not want.
You can create custom model binder for binding string arrays like this:
public class StringArrayBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
string key = bindingContext.ModelName;
ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (val != null && string.IsNullOrEmpty(val.AttemptedValue) == false)
{
bindingContext.ModelState.SetModelValue(key, val);
string incomingString = ((string[])val.RawValue)[0];
var splitted = incomingString.Split(',');
if (splitted.Length > 1)
{
return splitted;
}
}
return null;
}
}
And then register it in global.asax on application startup:
ModelBinders.Binders[typeof(string[])] = new StringArrayBinder();
Or even simpler but less reusable approach would be:
public string[] MyStringPropertyArray { get; set; }
public string MyStringProperty
{
get
{
if (MyStringPropertyArray != null)
return string.Join(",", MyStringPropertyArray);
return null;
}
set
{
if (!string.IsNullOrWhiteSpace(value))
{
MyStringPropertyArray = value.Split(',');
}
else
{
MyStringPropertyArray = null;
}
}
}
Here you would bind to MyStringProperty in the view. And then use MyStringPropertyArray (populated with values from MyStringProperty) in your business code.
I've written an If-IsRequired custom attribute to validate that a property contains a value depending on the values of some other properties in the model. Since I want to make this attribute apply to as many situations as possible, I want to allow the option for the developer leveraging the attribute to supply an infinite number of matched parameters. And lastly, I want to be able to enforce that all the parameters are matched correctly.
This is what I've written thus far. While I'm currently using arrays of strings, I'd be perfectly happy to use some sort of collection, which been unable to work. In addition, I now have a need to support the current attribute definition and create a new overload that includes the comparison operator. This will allow me to make less than, greater than, and not equal comparisons in addition to the original definition which just assumes all comparisons are done with equals.
/// <summary>
/// A custom attribute that checks the value of other properties passed to it in order to
/// determine if the property this attribute is bound to should be required.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class IsPropertyRequiredAttribute : ValidationAttribute
{
private const string DefaultErrorMessage = "{0} is required.";
public string[] _selectionContextNames { get; private set; }
public string[] _expectedValues { get; private set; }
/// <summary>
/// Creates a new instance of the IsPropertyRequriedAttribute.
/// </summary>
/// <param name="SelectionContextNames">The name of the other property in the view model to check the value of.</param>
/// <param name="ExpectedValues">The expected value of the other property in the view model in order to determine if the current property the attribute is bound to should be required.</param>
public IsPropertyRequiredAttribute(string[] SelectionContextNames, string ExpectedValues)
: base(DefaultErrorMessage)
{
_selectionContextNames = SelectionContextNames;
_expectedValues = ExpectedValues;
}
public override bool IsValid(object value)
{
if (_selectionContextNames == null || _expectedValues == null)
{
if (_selectionContextNames != null || _expectedValues != null)
{
string paramName;
if (_selectionContextNames == null)
{
paramName = "ExpectedValues";
}
else
{
paramName = "SelectionContextNames";
}
throw new ArgumentException("Key/Value pairs need to match for IsPropertyRequired.", paramName);
}
}
else if (_selectionContextNames.Length != _expectedValues.Length)
{
string paramName;
if (_selectionContextNames.Length < _expectedValues.Length)
{
paramName = "ExpectedValues";
}
else
{
paramName = "SelectionContextNames";
}
throw new ArgumentException("Parameter element counts need to match for IsPropertyRequired.", paramName);
}
bool paramsValid = true;
if (_selectionContextName!= null)
{
for (int i = 0; i < _selectionContextName.Length; i++)
{
string paramValue = HttpContext.Current.Request[_selectionContextName[i]];
if (_expectedValue[i] != paramValue)
{
paramsValid = false;
}
}
if (paramsValid == true)
{
return (value != null);
}
else
{
return true;
}
}
else
{
return true;
}
}
public override string FormatErrorMessage(string name)
{
return String.Format(DefaultErrorMessage, name);
}
}
While using the attribute to decorate the property will depend on how the attribute is defined, this is what I have currently implemented (which could also probably be improved):
[IsPropertyRequired(new string[] {"prop1", "prop2", "prop3", "prop4"}, new string[] {"1", "2", "3", "4"})]
public string SomeText { get; set; }
Also, I want to prevent, as much as I can, the following decoration from happening:
[IsPropertyRequired(new string[] {"prop1", "prop2", "prop3", "prop4", "prop5withoutvalue"}, new string[] {"1", "2", "3", "4"})]
public string SomeOtherText { get; set; }
And with the new overload including comparison operators as a parameter, we could now have:
[IsPropertyRequired(new string[] {"prop1", "prop2", "prop3", "prop4"}, new string[] {"==", ">", "!=", "<="}, new string[] {"1", "2", "3", "4"})]
public string SomeComparisonText { get; set; }
Attributes in .NET are very limited in the allowed types you can specify, as mentioned on MSDN. If you want more complex data to be specified, I would recommend writing the attribute to specify an alternate location for the richer data structure.
For example, imagine an attribute with this syntax:
[ValidationRules(typeof(MyValidationRuleInfo, "MyRuleSet")]
public int SomeProperty { get; set; }
...
public static class MyValidationRuleInfo {
public static Dictionary<string, ValidationRule> MyRuleSet {
get {
return new { ... rules go here ... }
}
}
And the attribute would look up the property on the target class and get all the rules there. It's still up to you to implement all the logic of all the rules, but you get to avoid attribute soup, and you also avoid unwieldy data structures.
In fact, the xUnit.NET unit testing library does something similar with its Theory and PropertyData attributes, as shown here.
I'm trying to use the DataAnnotationsModelBinder in order to use data annotations for server-side validation in ASP.NET MVC.
Everything works fine as long as my ViewModel is just a simple class with immediate properties such as
public class Foo
{
public int Bar {get;set;}
}
However, the DataAnnotationsModelBinder causes a NullReferenceException when trying to use a complex ViewModel, such as
public class Foo
{
public class Baz
{
public int Bar {get;set;}
}
public Baz MyBazProperty {get;set;}
}
This is a big problem for views that render more than one LINQ entity because I really prefer using custom ViewModels that include several LINQ entities instead of untyped ViewData arrays.
The DefaultModelBinder does not have this problem, so it seems like a bug in DataAnnotationsModelBinder. Is there any workaround to this?
Edit: A possible workaround is of course to expose the child object's properties in the ViewModel class like this:
public class Foo
{
private Baz myBazInstance;
[Required]
public string ExposedBar
{
get { return MyBaz.Bar; }
set { MyBaz.Bar = value; }
}
public Baz MyBaz
{
get { return myBazInstance ?? (myBazInstance = new Baz()); }
set { myBazInstance = value; }
}
#region Nested type: Baz
public class Baz
{
[Required]
public string Bar { get; set; }
}
#endregion
}
#endregion
But I'd prefer not to have to write all this extra code. The DefaultModelBinder works fine with such hiearchies, so I suppose the DataAnnotationsModelBinder should as well.
Second Edit: It looks like this is indeed a bug in DataAnnotationsModelBinder. However, there is hope this might be fixed before the next ASP.NET MVC framework version ships. See this forum thread for more details.
I faced the exact same issue today. Like yourself I don't tie my View directly to my Model but use an intermediate ViewDataModel class that holds an instance of the Model and any parameters / configurations I'd like to sent of to the view.
I ended up modifying BindProperty on the DataAnnotationsModelBinder to circumvent the NullReferenceException, and I personally didn't like properties only being bound if they were valid (see reasons below).
protected override void BindProperty(ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor) {
string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
// Only bind properties that are part of the request
if (bindingContext.ValueProvider.DoesAnyKeyHavePrefix(fullPropertyKey)) {
var innerContext = new ModelBindingContext() {
Model = propertyDescriptor.GetValue(bindingContext.Model),
ModelName = fullPropertyKey,
ModelState = bindingContext.ModelState,
ModelType = propertyDescriptor.PropertyType,
ValueProvider = bindingContext.ValueProvider
};
IModelBinder binder = Binders.GetBinder(propertyDescriptor.PropertyType);
object newPropertyValue = ConvertValue(propertyDescriptor, binder.BindModel(controllerContext, innerContext));
ModelState modelState = bindingContext.ModelState[fullPropertyKey];
if (modelState == null)
{
var keys = bindingContext.ValueProvider.FindKeysWithPrefix(fullPropertyKey);
if (keys != null && keys.Count() > 0)
modelState = bindingContext.ModelState[keys.First().Key];
}
// Only validate and bind if the property itself has no errors
//if (modelState.Errors.Count == 0) {
SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue)) {
OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
}
//}
// There was an error getting the value from the binder, which was probably a format
// exception (meaning, the data wasn't appropriate for the field)
if (modelState.Errors.Count != 0) {
foreach (var error in modelState.Errors.Where(err => err.ErrorMessage == "" && err.Exception != null).ToList()) {
for (var exception = error.Exception; exception != null; exception = exception.InnerException) {
if (exception is FormatException) {
string displayName = GetDisplayName(propertyDescriptor);
string errorMessage = InvalidValueFormatter(propertyDescriptor, modelState.Value.AttemptedValue, displayName);
modelState.Errors.Remove(error);
modelState.Errors.Add(errorMessage);
break;
}
}
}
}
}
}
I also modified it so that it always binds the data on the property no matter if it's valid or not. This way I can just pass the model back to the view withouth invalid properties being reset to null.
Controller Excerpt
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(ProfileViewDataModel model)
{
FormCollection form = new FormCollection(this.Request.Form);
wsPerson service = new wsPerson();
Person newPerson = service.Select(1, -1);
if (ModelState.IsValid && TryUpdateModel<IPersonBindable>(newPerson, "Person", form.ToValueProvider()))
{
//call wsPerson.save(newPerson);
}
return View(model); //model.Person is always bound no null properties (unless they were null to begin with)
}
My Model class (Person) comes from a webservice so I can't put attributes on them directly, the way I solved this is as follows:
Example with nested DataAnnotations
[Validation.MetadataType(typeof(PersonValidation))]
public partial class Person : IPersonBindable { } //force partial.
public class PersonValidation
{
[Validation.Immutable]
public int Id { get; set; }
[Validation.Required]
public string FirstName { get; set; }
[Validation.StringLength(35)]
[Validation.Required]
public string LastName { get; set; }
CategoryItemNullable NearestGeographicRegion { get; set; }
}
[Validation.MetadataType(typeof(CategoryItemNullableValidation))]
public partial class CategoryItemNullable { }
public class CategoryItemNullableValidation
{
[Validation.Required]
public string Text { get; set; }
[Validation.Range(1,10)]
public string Value { get; set; }
}
Now if I bind a form field to [ViewDataModel.]Person.NearestGeographicRegion.Text & [ViewDataModel.]Person.NearestGeographicRegion.Value the ModelState starts validating them correctly and DataAnnotationsModelBinder binds them correctly as well.
This answer is not definitive, it's the product of scratching my head this afternoon.
It's not been properly tested, eventhough it passed the unit tests in the project Brian Wilson started and most of my own limited testing. For true closure on this matter I would love to hear Brad Wilson thoughts on this solution.
The fix for this issue is simple, as Martijn has noted.
In the BindProperty method, you will find this line of code:
if (modelState.Errors.Count == 0) {
It should be changed to:
if (modelState == null || modelState.Errors.Count == 0) {
We are intending to include DataAnnotations support in MVC 2, which will include the DataAnnotationsModelBinder. This feature will be part of the first CTP.