ASP.NET MVC binding decimal value

后端 未结 4 2026
既然无缘
既然无缘 2021-01-04 00:19

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

相关标签:
4条回答
  • 2021-01-04 01:06

    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);
            }
        }
    }
    
    0 讨论(0)
  • 2021-01-04 01:10

    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);
    
    0 讨论(0)
  • 2021-01-04 01:12

    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.

    0 讨论(0)
  • 2021-01-04 01:16

    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)));
    
    0 讨论(0)
提交回复
热议问题