I'm trying to figure out why framework refuses to bind "1,234.00" value to decimal. What can be the reason for it?
Values like "123.00" or "123.0000" bind successfully.
I have the following code setting my culture config in Global.asax
public void Application_AcquireRequestState(object sender, EventArgs e)
{
var culture = (CultureInfo)Thread.CurrentThread.CurrentCulture.Clone();
culture.NumberFormat.NumberDecimalSeparator = culture.NumberFormat.CurrencyDecimalSeparator = culture.NumberFormat.PercentDecimalSeparator = ".";
culture.NumberFormat.NumberGroupSeparator = culture.NumberFormat.CurrencyGroupSeparator = culture.NumberFormat.PercentGroupSeparator = ",";
Thread.CurrentThread.CurrentCulture = culture;
}
French culture is set as default culture in Web.Config
<globalization uiCulture="fr-FR" culture="fr-FR" />
I've dived into sources of System.Web.Mvc.dll's ValueProviderResult class. It is using System.ComponentModel.DecimalConverter.
converter.ConvertFrom((ITypeDescriptorContext) null, culture, value)
Here is where the message "1,234.0000 is not a valid value for Decimal." comes from.
I've tried to run the following code in my playground:
static void Main()
{
var decConverter = TypeDescriptor.GetConverter(typeof(decimal));
var culture = new CultureInfo("fr-FR");
culture.NumberFormat.NumberDecimalSeparator = culture.NumberFormat.CurrencyDecimalSeparator = culture.NumberFormat.PercentDecimalSeparator = ".";
culture.NumberFormat.NumberGroupSeparator = culture.NumberFormat.CurrencyGroupSeparator = culture.NumberFormat.PercentGroupSeparator = ",";
Thread.CurrentThread.CurrentCulture = culture;
var d1 = Decimal.Parse("1,232.000");
Console.Write("{0}", d1); // prints 1234.000
var d2 = decConverter.ConvertFrom((ITypeDescriptorContext)null, culture, "1,232.000"); // throws "1,234.0000 is not a valid value for Decimal."
Console.Write("{0}", d2);
}
DecimalConverter throws same exception. Decimal.Parse correctly parses the same string.
The problem is, that DecimalConverter.ConvertFrom
does not support the AllowThousands
flag of the NumberStyles
enumeration when it calls Number.Parse
. The good news is, that there exists a way to "teach" it to do so!
Decimal.Parse
internally calls Number.Parse
with the number style set to Number
, for which the AllowThousands
flag is set to true.
[__DynamicallyInvokable]
public static decimal Parse(string s)
{
return Number.ParseDecimal(s, NumberStyles.Number, NumberFormatInfo.CurrentInfo);
}
When you are receiving a type converter from the descriptor, you actually get an instance of DecimalConverter
. The ConvertFrom
method is a kinda general and large, so I only quote the relevant parts for the current scenario here. The missing parts are implementing support for hex strings and exception handling.1
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string)
{
// ...
string text = ((string)value).Trim();
if (culture == null)
culture = CultureInfo.CurrentCulture;
NumberFormatInfo formatInfo = (NumberFormatInfo)culture.GetFormat(typeof(NumberFormatInfo));
return FromString(text, formatInfo);
// ...
}
return base.ConvertFrom(context, culture, value);
}
DecimalConverter
also overwrites the FromString
implementation and there the problem raises:
internal override object FromString(string value, NumberFormatInfo formatInfo)
{
return Decimal.Parse(value, NumberStyles.Float, formatInfo);
}
With the number style set to Float
, the AllowThousands
flag is set to false! However you can write a custom converter with a few lines of code that fixes this issue.
class NumericDecimalConverter : DecimalConverter
{
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string)
{
string text = ((string)value).Trim();
if (culture == null)
culture = CultureInfo.CurrentCulture;
NumberFormatInfo formatInfo = (NumberFormatInfo)culture.GetFormat(typeof(NumberFormatInfo));
return Decimal.Parse(text, NumberStyles.Number, formatInfo);
}
else
{
return base.ConvertFrom(value);
}
}
}
1Note that the code looks similar to the original implementation. If you need the "unquoted" stuff either delegate it directly to base
or implement it on your own. You can view the implementation using ILSpy/DotPeek/etc. or by debugging into them from Visual Studio.
Finally, with a little help from Reflection, you can set the type converter for Decimal
to use your new custom one!
TypeDescriptor.AddAttributes(typeof(decimal), new TypeConverterAttribute(typeof(NumericDecimalConverter)));
Based on a comment from an article about decimal model binding by Phil Haack here, I believe part of the answer to the "why" is that culture in browsers is complicated and you can't be guaranteed that your application's culture will be the same culture settings used by the user/ browser for decimals. In any case it is a known "issue" and similar questions have been asked before with a variety of solutions offered, in addition to the so: Accept comma and dot as decimal separator and How to set decimal separators in ASP.NET MVC controllers? for example.
You can try overriding the DefaultModelBinder. Let me know if this doesn't work and I'll delete this post. I didn't actually put together an MVC app and test it, but based on experience this should work:
public class CustomModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
if(propertyDescriptor.PropertyType == typeof(decimal))
{
propertyDescriptor.SetValue(bindingContext.Model, double.Parse(propertyDescriptor.GetValue(bindingContext.Model).ToString()));
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
else
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
}
The issue here appears to be the default Number Styles applied to Decimal.Parse(string).
From MSDN documentation
The remaining individual field flags define style elements that may be, but do not have to be, present in the string representation of a decimal number for the parse operation to succeed.
So this means that both d1 and d2 below successfully parse
var d1 = Decimal.Parse("1,232.000");
var d2 = Decimal.Parse("1,232.000", NumberStyles.Any);
However when applying the type convertor it appears that this essentially only allows the allow training spaces, allow decimal point and allow leading sign. As such the d3 express below will throw a runtime error
var d3 = Decimal.Parse("1,232.000", NumberStyles.AllowLeadingSign | NumberStyles.AllowLeadingWhite |
NumberStyles.AllowTrailingWhite | NumberStyles.AllowDecimalPoint);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With