Default ASP.NET MVC 3 model binder doesn't bind decimal properties - asp.net-mvc-3

For some reason, when I send this JSON to an action:
{"BaseLoanAmount": 5000}
which is supposed to be bound to a model with a decimal property named "BaseLoanAmount", it doesn't bind, it just stays 0. But if I send:
{"BaseLoanAmount": 5000.00}
it does bind the property, but why? Can't 5000 be converted to a decimal event if it doesn't have decimal numbers?

After stepping into asp.net mvc's source code, it seemsd the problem is that for the conversion asp.net mvc uses the framework's type converter, which for some reason returns false for an int to decimal conversion, I ended up using a custom model binder provider and model binder for decimals, you can see it here:
public class DecimalModelBinder : DefaultModelBinder
{
#region Implementation of IModelBinder
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult.AttemptedValue.Equals("N.aN") ||
valueProviderResult.AttemptedValue.Equals("NaN") ||
valueProviderResult.AttemptedValue.Equals("Infini.ty") ||
valueProviderResult.AttemptedValue.Equals("Infinity") ||
string.IsNullOrEmpty(valueProviderResult.AttemptedValue))
return 0m;
return Convert.ToDecimal(valueProviderResult.AttemptedValue);
}
#endregion
}
To register this ModelBinder, just put the following line inside Application_Start():
ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
ModelBinders.Binders.Add(typeof(decimal?), new DecimalModelBinder());

Try sending it like this:
{ "BaseLoanAmount": "5000" }

Related

Validate all strings

I want to prohibit the use of the | character for all strings submitted to my website but I don't want to have to apply a validator attribute to every string property because its unmanageable.
I could validate all strings in a model binder (I'm currently using one to trim all strings) but I don't think that would integrate with the standard validation framework. i.e. generating client side validation.
Any ideas how to do this?
I didn't get this working on the client however I can trap all dodgy strings in a custom model binder and make the form invalid with the following...
public class CustomStringModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueResult == null || string.IsNullOrEmpty(valueResult.AttemptedValue))
{
return null;
}
if (valueResult.AttemptedValue.Contains("|"))
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "The | character is prohibited.");
}
return valueResult.AttemptedValue.Trim();
}
}

Validating a view model after custom model binding

I have a view model that implements IValidatableObject that contains a string and a collection of another view model, something like this:
public sealed class MainViewModel
{
public string Name { get; set; }
public ICollection<OtherViewModel> Others { get; set; }
}
My validation checks each object in Others against different rules using the contract provided by IValidatableObject:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
foreach (var other in this.Others)
{
// validate or yield return new ValidationResult
}
}
Because of the complex structure of the real MainViewModel I have had to create a custom model binder which re-builds the model and assigns POST data to the relevant components. The problem that I'm getting is that nothing is getting validated resulting in validation errors at the context level as it violates certain database constraints and I'm not sure what I'm doing wrong - I assumed that ModelState.IsValid would invoke the Validate method on my view model but it doesn't seem to go down that way.
My model binder looks like this:
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
int modelId = (int)controllerContext.RouteData.Values["id"];
// query the database and re-build the components of the view model
// iterate the POST data and assign to the model where necessary
// should I be calling something here to validate the model before it's passed to the controller?
return model;
}
Any help appreciated!
Validator.TryValidateObject
OK, seems I'm a little closer. I can now get my IValidatableObject method to run by adding the following to my custom model binder:
var validationResults = new HashSet<ValidationResult>();
var isValid = Validator.TryValidateObject(model, new ValidationContext(model, null, null), validationResults, true);
Seems that Validator.TryValidateObject invokes the validation method and setting the last parameter to true causes it to validate all properties. However, I'm now stuck with getting the validationResults to the controller so they can be used in a meaningful way.
I should have realised that I could use the ModelState.AddModelError through a custom binder, I've managed to get this working correctly now by adding the following to my custom model binder before returning the model to the controller:
var validationResults = new HashSet<ValidationResult>();
var isValid = Validator.TryValidateObject(model, new ValidationContext(model, null, null), validationResults, true);
if (!isValid)
{
foreach (var result in validationResults)
{
bindingContext.ModelState.AddModelError("", result.ErrorMessage);
}
}
return model;
This now returns a list of all errors to my page and the ModelState.IsValid check on my controller action is now returning false.
Paul's great answer can be refactored into a generic validate-and-convert to ModelState method as follows (e.g. in a helper or CustomModelBinder base). In addition, the bindings to the validated properties are retained.
public static void DoValidation(ModelBindingContext bindingContext,
IValidatableObject model)
{
var validationResults = new HashSet<ValidationResult>();
var isValid = Validator.TryValidateObject(model,
new ValidationContext(model, null, null), validationResults, true);
if (!isValid)
{
var resultsGroupedByMembers = validationResults
.SelectMany(_ => _.MemberNames.Select(
x => new {MemberName = x ?? "",
Error = _.ErrorMessage}))
.GroupBy(_ => _.MemberName);
foreach (var member in resultsGroupedByMembers)
{
bindingContext.ModelState.AddModelError(
member.Key,
string.Join(". ", member.Select(_ => _.Error)));
}
}
}

ASP.NET MVC3 ValueProvider drops string input to a double property

I'm attempting to validate the input of a text box which corresponds to a property of type double in my model. If the user inputs "foo" I want to know about it so I can display an error. However, the ValueProvider is dropping the value silently (no errors are added to the ModelState).
In a normal submission, I fill in "2" for the text box corresponding to myDouble and submit the form.
Inspecting controllerContext.HttpContext.Request.Form shows that myDouble=2, among other correct inputs. bindingContext.ValueProvider.GetValue("myDouble") == 2, as expected. The bindingContext.ModelState.Count == 6 and bindingContext.ModelState["myDouble"].Errors.Count == 0. Everything is good and the model binds as expected.
Then I fill in "foo" for the text box corresponding to myDouble and submitted the form.
Inspecting controllerContext.HttpContext.Request.Form shows that myDouble=foo, which is what I expected. However, bindingContext.ValueProvider.GetValue("myDouble") == null and bindingContext.ModelState.Count == 5 (The exact number isn't important, but it's one less than before). Looking at the ValueProvider, is as if myDouble was never submitted and the model binding occurs as if it wasn't. This makes it difficult to differentiate between a bad input and no input.
Is this the expected behavior of ValueProvider? Is there a way to get ValueProvider to report when conversion fails without implementing a custom ValueProvider? Thanks!
Part of the problem here is that your model has a type of double.
The problem is that double cannot be null, and as such will default to a value of 0, thus on submit.. if the ValueProvider returns null, the value of the field will still be 0 and validation will pass.
You should make the double nullable, by using double? and then add a Required attribute to the property. If the type is not required, then you can add a regular expression validator.
You can implement custom model binding logic using by implementing IModelBinder. This will put the data validation logic at the model binding level - thus being usable for any type of ValueProvider. In your situation, the model binder would determine that when myDouble = "foo" is not a double and add an exception to the ModelState errors showing the invalid value.
public class CustomDoubleBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
decimal tempDouble = 0m;
if (bindingContext.ValueProvider.GetValue(bindingContext.ModelName) != null)
{
if (double.TryParse(bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue, out tempDecimal))
{
bindingContext.ModelState[bindingContext.ModelName].Errors.Add("Error parsing double value: " + bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue);
}
}
return tempDouble;
}
}
Having created this custom model binder, you will then need to register it in the Global.asax:
protected void Application_Start()
{
ModelBinders.Binders[typeof(double)] = new CustomDoubleBinder();
}

Dynamic Form Building and Passing Query Parameters

I am working on a form that is generated dynamically based on some meta-data tables in my database. I create input tags with names like setting_1, setting_53, setting_22 where the number is the primary key of the meta-data. Since the content is dynamic, I am using FormCollection as the sole parameter on POST requests.
Question 1: Is there a FormCollection-like class for GET requests? I want direct access to the query parameters.
Question 2: If I need to pass these query parameters around, is there an easy/safe way to build my URLs?
One of my big concerns is that some of the settings are populated via OAuth, so the user will be redirected to an external page. I will have to pass the query string as "state" which I will need to recover once the user returns. I will need to use this state to pick up where the user left off in the form entry process. All the more reason why I need a very fool-proof mechanism for passing query parameters around.
Has anyone dealt with dynamic pages like these? Are there good patterns and practices for passing these pages around?
Well, you can certainly look at Request.QueryString inside of a controller action.
But if it were me doing it, I'd write a custom model binder instead.
Here's a sample model binder. I haven't tested this!
public class MyModelBinder: DefaultModelBinder
{
private static void BindSettingProperty(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.PropertyType != typeof(IDictionary<string, string>))
{
throw new InvalidOperationException("This binder is for setting dictionaries only.");
}
var originalValue = propertyDescriptor.GetValue(bindingContext.Model) as IDictionary<string, string>;
var value = originalValue ?? new Dictionary<string, string>();
var settingKeys = controllerContext.HttpContext.Request.QueryString.AllKeys.Where(k => k.StartsWith("setting_", StringComparison.OrdinalIgnoreCase));
foreach (var settingKey in settingKeys)
{
var key = settingKey.Substring(8);
value.Add(key, bindingContext.ValueProvider.GetValue(settingKey).AttemptedValue);
}
if (value.Any() && (originalValue == null))
{
propertyDescriptor.SetValue(bindingContext.Model, value);
}
}
protected override void BindProperty(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.Name.StartsWith("setting_", StringComparison.OrdinalIgnoreCase)
{
BindSettingProperty(controllerContext, bindingContext, propertyDescriptor);
}
else
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
}

ASP.NET MVC datetime culture issue when passing value back to controller

How can i tell my controller/model what kind of culture it should expect for parsing a datetime?
I was using some of this post to implement jquery datepicker into my mvc application.
When i submit the date it gets "lost in translation" i'm not using the US formatting for the date, so when it gets sent to my controller it simply becomes null.
I have a form where the user chooses a date:
#using (Html.BeginForm("List", "Meter", FormMethod.Get))
{
#Html.LabelFor(m => m.StartDate, "From:")
<div>#Html.EditorFor(m => m.StartDate)</div>
#Html.LabelFor(m => m.EndDate, "To:")
<div>#Html.EditorFor(m => m.EndDate)</div>
}
I've made an edit template for this, to implement the jquery datepicker:
#model DateTime
#Html.TextBox("", Model.ToString("dd-MM-yyyy"), new { #class = "date" })
I then create the datepicker widgets like this.
$(document).ready(function () {
$('.date').datepicker({ dateFormat: "dd-mm-yy" });
});
All this works fine.
Here is where the problems start, this is my controller:
[HttpGet]
public ActionResult List(DateTime? startDate = null, DateTime? endDate = null)
{
//This is where startDate and endDate becomes null if the dates dont have the expected formatting.
}
This is why i would like to somehow tell my controller what culture it should expect?
Is my model wrong? can i somehow tell it which culture to use, like with the data annotation attributes?
public class MeterViewModel {
[Required]
public DateTime StartDate { get; set; }
[Required]
public DateTime EndDate { get; set; }
}
Edit: this link explains my issue and a very good solution to it aswell. Thanks to gdoron
you can change the default model binder to use the user culture using IModelBinder
public class DateTimeBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var date = value.ConvertTo(typeof(DateTime), CultureInfo.CurrentCulture);
return date;
}
}
And in the Global.Asax write:
ModelBinders.Binders.Add(typeof(DateTime), new DateTimeBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeBinder());
Read more at this excellent blog that describe why Mvc framework team implemented a default Culture to all users.
You can create a Binder extension to handle the date in the culture format.
This is a sample I wrote to handle the same problem with Decimal type, hope you get the idea
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
{
actualValue = Convert.ToDecimal(valueResult.AttemptedValue, CultureInfo.CurrentCulture);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
Update
To use it simply declare the binder in Global.asax like this
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
//HERE you tell the framework how to handle decimal values
ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
DependencyResolver.SetResolver(new ETAutofacDependencyResolver());
}
Then when the modelbinder has to do some work, it will know automatically what to do.
For example, this is an action with a model containing some properties of type decimal. I simply do nothing
[HttpPost]
public ActionResult Edit(int id, MyViewModel viewModel)
{
if (ModelState.IsValid)
{
try
{
var model = new MyDomainModelEntity();
model.DecimalValue = viewModel.DecimalValue;
repository.Save(model);
return RedirectToAction("Index");
}
catch (RulesException ex)
{
ex.CopyTo(ModelState);
}
catch
{
ModelState.AddModelError("", "My generic error message");
}
}
return View(model);
}
This issue arises because you are using the GET method on your Form. The QueryString Value Provider in MVC always uses Invariant/US date format. See: MVC DateTime binding with incorrect date format
There are three solutions:
Change your method to POST.
As someone else says, change the date format to ISO 8601 "yyyy-mm-dd" before submission.
Use a custom binder to always treat Query String dates as GB. If you do this you have to make sure that all dates are in that form:
public class UKDateTimeModelBinder : IModelBinder
{
private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
/// <summary>
/// Fixes date parsing issue when using GET method. Modified from the answer given here:
/// https://stackoverflow.com/questions/528545/mvc-datetime-binding-with-incorrect-date-format
/// </summary>
/// <param name="controllerContext">The controller context.</param>
/// <param name="bindingContext">The binding context.</param>
/// <returns>
/// The converted bound value or null if the raw value is null or empty or cannot be parsed.
/// </returns>
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var vpr = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (vpr == null)
{
return null;
}
var date = vpr.AttemptedValue;
if (String.IsNullOrEmpty(date))
{
return null;
}
logger.DebugFormat("Parsing bound date '{0}' as UK format.", date);
// Set the ModelState to the first attempted value before we have converted the date. This is to ensure that the ModelState has
// a value. When we have converted it, we will override it with a full universal date.
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, bindingContext.ValueProvider.GetValue(bindingContext.ModelName));
try
{
var realDate = DateTime.Parse(date, System.Globalization.CultureInfo.GetCultureInfoByIetfLanguageTag("en-GB"));
// Now set the ModelState value to a full value so that it can always be parsed using InvarianCulture, which is the
// default for QueryStringValueProvider.
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, new ValueProviderResult(date, realDate.ToString("yyyy-MM-dd hh:mm:ss"), System.Globalization.CultureInfo.GetCultureInfoByIetfLanguageTag("en-GB")));
return realDate;
}
catch (Exception)
{
logger.ErrorFormat("Error parsing bound date '{0}' as UK format.", date);
bindingContext.ModelState.AddModelError(bindingContext.ModelName, String.Format("\"{0}\" is invalid.", bindingContext.ModelName));
return null;
}
}
}
When submitting a date you should always try and submit it in the format "yyyy-MM-dd". This will allow for it to become culture independent.
I normally have a hidden field which maintains the date in this format. This is relatively simple using jQuery UI's datepicker.
Why not simply inspect the culture of the data and convert it as such? This simple approach allowed me to use strongly typed dates in models, show action links and edit fields in the desired locale and not have to fuss at all binding it back into a strongly typed DateTime:
public class DateTimeBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
return value.ConvertTo(typeof(DateTime), value.Culture);
}
}
that did the trick for me
<system.web>
<globalization enableClientBasedCulture="true" uiCulture="Auto" culture="Auto" />
</system.web>
I have a updated solution for MVC5 based on the Post of #gdoron. I will share it in case anyone else is looking for this. The class inherits from DefaultModelBinder and has exception handling for invalid dates. It also can handle null values:
public class DateTimeModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object result = null;
var modelName = bindingContext.ModelName;
var attemptedValue = bindingContext.ValueProvider.GetValue(modelName)?.AttemptedValue;
// in datetime? binding attemptedValue can be Null
if (attemptedValue != null && !string.IsNullOrWhiteSpace(attemptedValue))
{
try
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
result = DateTime.Parse(value.AttemptedValue, CultureInfo.CurrentCulture);
}
catch (FormatException e)
{
bindingContext.ModelState.AddModelError(modelName, e);
}
}
return result;
}
}
And just like the mentioned sample in the Global.Asax write
ModelBinders.Binders.Add(typeof(DateTime), new DateTimeBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeBinder());

Resources