Model Binding Decimal Values

I’m in the beautiful country of Brazil right now (I’ll hopefully blog more about that later) proctoring for the hands-on labs that’s part of the Web Camps agenda.

However, the folks here are keeping me on my toes asking me to give impromptu and deeply advanced demos. It almost feels like a form of performance art as I create brand new demos on the fly. Smile

During this time, several people reported issues binding to a decimal value that prompted me to write a new demo and this blog post.

Let’s look at the scenario. Suppose you have the following class (Jogador is a soccer player in Portugese):

public class Jogador {
    public int ID { get; set; }
        
    public string Name { get; set; }
        
    public decimal Salary { get; set; }
}

And you have two controller actions, one that renders a form used to create a Jogador and another action method that receives the POST request.

public ActionResult Create() {
  // Code inside here is not important
  return View();
}

public ActionResult Create(Jogador player) {
  // Code inside here is not important
  return View();  
}

When you type in a value such as 1234567.55 into the Salary field and try to post it, it works fine. But typically, you would want to type it like 1,234,567.55 (or here in Brazil, you would type it as 1.234.567,55).

In that case, the DefaultModelBinder chokes on the value. This is unfortunate because jQuery Validate allows that value just fine. I’ll talk to the rest of my team about whether we should fix this in the next version of ASP.NET MVC, but for now it’s good to know there’s a workaround.

In general, we recommend folks don’t write custom model binders because they’re difficult to get right and they’re rarely needed. The issue I’m discussing in this post might be one of those cases where it’s warranted.

Here’s the code for my DecimalModelBinder. I should probably write one for other decimal types too, but I’m lazy.

WARNING: This is sample code! I haven’t tried to optimize it or test all scenarios. I know it works for direct decimal arguments to action methods as well as decimal properties when binding to complex objects.

using System;
using System.Globalization;
using System.Web.Mvc;

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

With this in place, you can easily register this in Application_Start within Global.asax.cs.

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();
    
    ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());

    // All that other stuff you usually put in here...
}

That registers our model binder to only be applied to decimal types, which is good since we wouldn’t want model binding to try and use this model binder when binding any other type.

With this in place, the Salary field will now accept both 1234567.55 and 1,234,567.55.

Hope you find this useful. I’ve had a great time in Buenos Aires, Argentina and São Paulo, Brazil. I’ll probably be swamped when I get back home, but I’ll try to make time to write about my time here.

What others have said

Requesting Gravatar... Alaor Mar 19, 2011 3:57 AM
# re: Model Binding Decimal Values
I was suffering with this issue past week. I live in Brazil and I was very upset because I couldn't make it work. Thanks for the post. ASP.NET MVC 3 is very nice, but sometimes it looks like the globalization features were put in second place.

p.s. I hope you can get here in Rio de Janeiro sometime in the future to talk to us.
Requesting Gravatar... jitendra Mar 19, 2011 4:18 AM
# re: Model Binding Decimal Values
I never faced this issue. But will remember your solution if i will face it in future. Thanks for the nice post.
Requesting Gravatar... Eduardo Mar 19, 2011 4:53 AM
# re: Model Binding Decimal Values
There is another thing.

Since PCs have a dot as a decimal point in the numeric pad, and calculators and other electronic devices use only the point as a decimal separator, a lot people in Argentina (and in other countries where the decimal separator is the comma) is used to sometimes type the point as a separator.

I have this function since long time to try to guess the user intentions (have worked great for me)

(old vb code, not optimized)

<Extension()> _
Public Function TryCDec(ByRef texto As String) As Decimal
If String.IsNullOrEmpty(texto) Then
Return 0
Else
Dim SepDecimal As String, SepMiles As String
texto = Replace(Trim(texto), "$", "")
texto = Replace(Trim(texto), " ", "")
If CDbl("3,24") = 324 Then
SepDecimal = "."
SepMiles = ","
Else
SepDecimal = ","
SepMiles = "."
End If
If InStr(texto, SepDecimal) > 0 Then
If InStr(texto, SepMiles) > 0 Then
If InStr(texto, SepDecimal) > InStr(texto, SepMiles) Then
texto = Replace(texto, SepMiles, "")
Else
texto = Replace(texto, SepDecimal, "")
texto = Replace(texto, SepMiles, SepDecimal)
End If
End If
Else
texto = Replace(texto, SepMiles, SepDecimal)
End If
Return CDec(texto)
End If
End Function


Requesting Gravatar... Guillaume Mar 19, 2011 5:53 AM
# What about client side validation ?
Thank you for your sample !
This work great for server side binding but what about client side validation when typing format like "1 234 567,55" ? it just doesn't work out of the box and it is really painfull.

do you have a "state of the art" workaround for this too ? we implement something but I'm quite unhappy with it.
Requesting Gravatar... Mike Mar 19, 2011 5:55 AM
# re: Model Binding Decimal Values
Why wouldn't you fix it in the version of MVC? This is a common issue and seems like it would be easy to fix in the default model binder.
Requesting Gravatar... Jesús López Mar 19, 2011 6:33 AM
# re: Model Binding Decimal Values
We use custom model binders for decimals and to trim strings. Our default model binder is a trimming model binder:

public class DefaultCustomBinder : DefaultModelBinder
{
protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
{
if (value != null && propertyDescriptor.PropertyType == typeof(string))
{
value = ((string)value).Trim();
if ((string)value == string.Empty)
{
value = null;
}
}
base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
}
}


And we use this model binder for decimals:

public class DecimalBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(decimal) || bindingContext.ModelType == typeof(decimal?))
{
return BindDecimal(bindingContext);
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}

private object BindDecimal(ModelBindingContext bindingContext)
{
ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == null)
{
return null;
}
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

decimal value;
string valueAsString = valueProviderResult.AttemptedValue == null ? null : valueProviderResult.AttemptedValue.Trim();
if (string.IsNullOrEmpty(valueAsString))
{
return null;
}
if (!decimal.TryParse(valueAsString, NumberStyles.Any, Thread.CurrentThread.CurrentCulture, out value))
{
var ex = new InvalidOperationException("Valor no válido", new Exception("Valor no válido", new FormatException("Valor no válido")));
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
return null;
}
return bindingContext.ModelType == typeof(decimal) ? value : new decimal?(value);
}
}
Requesting Gravatar... Daniel Mar 19, 2011 7:49 AM
# re: Model Binding Decimal Values
Actually jogador means "player" ;)
Requesting Gravatar... Luis Garcia Mar 20, 2011 4:03 AM
# re: Model Binding Decimal Values
Happy trip on the Air Force One.
Requesting Gravatar... Daniel Kolman Mar 20, 2011 6:33 AM
# re: Model Binding Decimal Values
Well, it is actually a bit more complicated. Culture you are using to convert the string into decimal (CultureInfo.CurrentCulture) can be different than culture in user's browser.
E.g. user has two keyboards and corresponding culture settings installed - german (with "," as decimal separator) and english (with "." as decimal separator). By switching the keyboard in browser (pressing alt+shift), character written by dot key in numeric keyboard changes. Regardless of your application's culture, browser sends either "123.5" or "123,5", depending on user's current keyboard. But both these values are perfectly valid from the user's point of view.
So then you have hard choice to make: either you accept both "," and "." as decimal separator (but then you do not allow thousands separator) or you are forcing user to input the number in culture expected by your application (but you are forcing the user to choose correct keyboard for your app).
Requesting Gravatar... Gabriel Mar 20, 2011 1:32 PM
# re: Model Binding Decimal Values
Hey Phil, nice to meet you at the Brasilian Web Camp, next time come with time to play a soccer game!

Thanks for coming and be sure to come back, next time you might consider going to Rio de Janeiro, there are lots of cabs there (lol)

Now for the problem on localization, there's also a problem on the JQuery validation plugin as it validates numbers with a en-US regular expression you can try it out on this link:
docs.jquery.com/Plugins/Validation/Methods/number

I found this bug on the ms connect site
connect.microsoft.com/...

on the ms connect site there's StackOverflow link and there's a workaround there: stackoverflow.com/...
I'm yet to test this workaround, But I know this is around JQuery Validation plugin, so I think its relevant.

Sorry for sending you so many links, but I hope it helps you.

Sorry for puting you on a hard position, the guys only wanted to enjoy the experience of meeting you the max. We really admire you.

Thanks for everything.
Gabriel
Requesting Gravatar... Paul Hiles Mar 20, 2011 9:47 PM
# re: Model Binding Decimal Values
In situations which require users to enter numbers in text fields, I personally prefer to use strings for properties on my viewmodel together with a regex validator that checks if the value is valid. That way, I always have complete control over the error message even when someone enters an invalid number which would typically cause the model binder to return a default error message that I have no control over. If you are using resource files for your error messages / regex, there is no need to a custom model binder.
Requesting Gravatar... Frank van Eykelen Mar 21, 2011 8:50 PM
# re: Model Binding Decimal Values
I'm working on a website that has two cultures (en-GB and nl-NL) and I don't have any problems with Decimal Model Binding.

Maybe it's because I set the Thread.CurrentThread.CurrentUICulture and Thread.CurrentThread.CurrentCulture to the culture that is used for the form input?

When the form is Dutch (nl-NL) they are set to {nl} and {nl-NL}, and when the form is English they are set to {en} and {en-GB}.
Requesting Gravatar... Alaor Mar 22, 2011 6:06 AM
# re: Model Binding Decimal Values
@Frank van Eykelen: You use the IsValid method in the controllers? Because the problem that he posted about (I think) is that, even if the form is using the correct culture and the jquery client validation is OK, when the IsValid method was called it returned a error because it couldn't bind the given decimal. So, if here you did type, for example, "1,01", the client validation would accept, but you would return to the page because of the server side validation.

At least this is what was happening to me, and this solution solved my problem.
Requesting Gravatar... Alaor Mar 22, 2011 6:09 AM
# re: Model Binding Decimal Values
Ooops, the IsValid is a property, not a method.
Requesting Gravatar... Frank van Eykelen Mar 22, 2011 5:27 PM
# re: Model Binding Decimal Values
@Alaor
- Yes, I use ModelState.IsValid in my HttpPost ActionResult.
- The ActionResult does not have a ValidateInput(false) attribute.
- In my class ProductMetadata I have this:

[DisplayName("Prijs excl. BTW")]
[DisplayFormat(ApplyFormatInEditMode = false, DataFormatString = "{0:C2}")]
public decimal Price { get; set; }


The DisplayFormat has - as one expects - no effect on the form post, it's only for displaying the value of the decimal.
Requesting Gravatar... Frank van Eykelen Mar 22, 2011 6:06 PM
# re: Model Binding Decimal Values
Sorry, I thought the problem concerned the decimal separator. I now see it's about the thousands separator. When I use those in a decimal field the validation indeed chokes.
Requesting Gravatar... Leniel Macaferi Apr 24, 2011 9:25 AM
# re: Model Binding Decimal Values
Hey Phil,

I'm having problems with the decimal separator ( , ) when dealing with jQuery unobtrusive validation.

Check this out if you can:

rebuildall.umbraworks.net/...

Thanks,

Leniel
Requesting Gravatar... StarTrekRedneck May 05, 2011 12:00 AM
# re: Model Binding Decimal Values
I had the same issue with enums-- when the default model binder recieves an integer, it will not use that value to fill a property that's an enum. >:( I could make an EnumModelBinder class that handles all enums, but I can't just register a model binder for the type "enum". Instead, I would have to register for each enum type there was...
ModelBinders.Binders.Add( typeof( CustomerTypeEnum), ...);
ModelBinders.Binders.Add( typeof( ProductTypeEnum), ...);
etc.
Ick! Perhaps if I could send the Binders.Add method a predicate<System.Type> then I could just "myType => myType.IsEnum;" But we can't.
Google pointed me to a post by Chuck Kinnan on the asp.net forums where he created a model binder that inherited from the default binder, overrode the protected GetPropertyValue() method, and handles the conversion there if the property type's IsEnum property is true. He's replacing the default binder with the inherited one. The post with code can be found here:
http://forums.asp.net/p/1622895/4180989.aspx
Requesting Gravatar... Brinkie Jun 15, 2011 7:14 PM
# re: Model Binding Decimal Values
For me this 'kinda' solved posting decimals to and MVC ActionMethod using ajax.

Only thing I changed to the binder was the actual parsing of the value, because in javascript a decimal is always represented in en-US format (31.2), so:

actualValue = Convert.ToDecimal(valueResult.AttemptedValue, new CultureInfo("en-US"));

instead of:

actualValue = Convert.ToDecimal(valueResult.AttemptedValue, CultureInfo.CurrentCulture);

Please note this would introduce issues when both decimals from form fields (say form input is in the nl-NL format with a decimal comma) and you also want do some ajax posting of a decimal to an MVC ActionMethod (e.g. using $.post() jQuery function) forcing a decimal dot there!
Requesting Gravatar... Travis Illig Jun 16, 2011 5:34 AM
# re: Model Binding Decimal Values
Was playing with this and ran into an issue.

If you do something in a controller like this...
return RedirectToAction("Success", new { Amount = decimalValue });
...then when the querystring gets generated, it comes out like this...
http://server/Controller/Success?Amount=1234.56
It's always formatted in InvariantCulture. I *think* I tracked this down to the System.Web.Routing.ParsedRoute.Bind() method.

The point is, if you use the culture-aware model binder and the culture uses the opposite decimal/thousands separator, you have a problem. 1234.56 is no more a valid format when dot is the thousands separator than 1234,56 is when comma is the separator. FormatException.

I'm not sure how to overcome this. It's not safe to do a try/catch on it because which would you try first? Invariant or Specific culture? What if you guess wrong?
Requesting Gravatar... Travis Illig Jun 16, 2011 6:05 AM
# re: Model Binding Decimal Values
You know what? I'm dumb. Disregard that. I was having some interesting and confusing problems when trying to apply the same logic to integer parsing and... let's just chalk it up to trying to work to fast, shall we? :S
Requesting Gravatar... Travis Illig Jun 16, 2011 8:23 AM
# re: Model Binding Decimal Values
Oh, wait, no, I was right - there is a problem with the querystring. You can solve it by using valueResult.Culture instead of CultureInfo.CurrentCulture when you parse the decimal.

valueResult.Culture will be CultureInfo.CurrentCulture for values posted by the Form (which will be from user input, ostensibly the culture-specific input) but will be CultureInfo.InvariantCulture for values present in the querystring (which get put there by that Bind() method I mentioned).
Requesting Gravatar... tobi Jun 16, 2011 9:43 PM
# re: Model Binding Decimal Values
I’ll talk to the rest of my team about whether we should fix this in the next version of ASP.NET MVC, but for now it’s good to know there’s a workaround.


hey phil,

please strike through "whether". you definitely have to fix this. better yesterday than tomorrow. it is very annoying coding in a different locale that has , instead of . !

...and please don't let us wait for mvc 4 for this ;)
thanks, tobi
Requesting Gravatar... Dean Jul 13, 2011 6:29 PM
# re: Model Binding Decimal Values
Thanks Travis, I will try that.

Can you include dates in this. The URL is generated with InvariantCulture, and this is not a good idea for dates, decimals, etc. There are perfectly good ISO standards for wire protocols.
Requesting Gravatar... Gabriel Jul 18, 2011 6:28 AM
# re: Model Binding Decimal Values
Awesome! Thanks so much! This was driving me insane today trying to get that Model Binding figured out. Thanks for the assistance!
Requesting Gravatar... Paulo Diogo Jul 24, 2011 9:36 AM
# re: Model Binding Decimal Values
hey man, thx...
Requesting Gravatar... b2berry Jul 30, 2011 4:47 AM
# re: Model Binding Decimal Values
Thanks for this solution! Definitely resolved my decimal issues.

I made a minor tweak to handle cases where I was using TryModelUpdate


if (bindingContext.ModelState.ContainsKey(bindingContext.ModelName))
bindingContext.ModelState[bindingContext.ModelName] = modelState;
else
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);


Without this check, the DecimalModelBinder would attempt to add the modelState when it already exists. :)
Requesting Gravatar... Jack Sep 20, 2011 9:10 PM
# re: Model Binding Decimal Values
yet more proof that MVC3 is a bad idea
just uninstall it and write HttpHandlers instead - then you can be productive again
Requesting Gravatar... Thiago Lang Nov 29, 2011 7:58 PM
# re: Model Binding Decimal Values
Hi,

Great post! But still, I don't know if I'm the only one that is having some problem with this but I just wanted for the Decimal model binder to accept both Comma and Dot as a decimal separator. Depending only of the user's input. If the User inputs a decimal value like this "1.1" or like this "1,1" the app would understand both standards. How can I do that?

Thanks in advance.
Requesting Gravatar... gee Dec 10, 2011 12:51 AM
# re: Model Binding Decimal Values
Doing a project in MVC and hating every minute of it. Longing to get back to Silverlight where you dont have to put up with all these hacks to get something to work. MVC 3 sucks big time. The time you save with view/controller generation is used up writing extension methods to cover up bugs in the ModelBinding, not to mention the miriad of bugs when dealing with decimals. Why deal with Json Ajax and jQuery when this is all taken care of for you in Silverlight. Doesnt make sense. Its a big step backwards.Of course, with IE10 not supporting plugins, its back to the stone age of javascript.
Requesting Gravatar... Nicolas Boisvert Jan 06, 2012 9:47 PM
# re: Model Binding Decimal Values
Used it and worked like a charm ! thanks a lot !
Requesting Gravatar... David Compton Mar 19, 2012 9:40 PM
# re: Model Binding Decimal Values
Used it and it worked beautifully - thanks Phil.

What do you have to say?

(will show your gravatar)
Please add 2 and 7 and type the answer here: