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 suc
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);
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.
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)));