ASP.NET MVC 绑定十进制值

ASP.NET MVC binding decimal value

我想弄清楚为什么框架拒绝将“1,234.00”值绑定到十进制。可能是什么原因?

“123.00”或“123.0000”等值绑定成功。

我有以下代码在 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;
    }

在 Web.Config

中,法国文化被设置为默认文化
  <globalization uiCulture="fr-FR" culture="fr-FR" />

我深入研究了 System.Web.Mvc.dll 的 ValueProviderResult class 的来源。它正在使用 System.ComponentModel.DecimalConverter。

converter.ConvertFrom((ITypeDescriptorContext) null, culture, value)

此处显示消息“1,234.0000 不是十进制的有效值。”来自.

我已经在我的 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 抛出相同的异常。 Decimal.Parse 正确解析相同的字符串。

您可以尝试覆盖 DefaultModelBinder。如果这不起作用,请告诉我,我将删除它 post。我实际上并没有组装 MVC 应用程序并对其进行测试,但根据经验,这应该可行:

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

例如,基于 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? 的一篇关于十进制模型绑定的文章的评论。

这里的问题似乎是应用于 Decimal.Parse(string) 的默认 Number Styles

来自 MSDN 文档

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.

所以这意味着下面的d1和d2都成功解析了

                var d1 = Decimal.Parse("1,232.000");

                var d2 = Decimal.Parse("1,232.000", NumberStyles.Any);

但是,在应用类型转换器时,这似乎基本上只允许允许训练空格、允许小数点和允许前导符号。因此,下面的 d3 express 将抛出运行时错误

                var d3 = Decimal.Parse("1,232.000", NumberStyles.AllowLeadingSign | NumberStyles.AllowLeadingWhite | 
                                        NumberStyles.AllowTrailingWhite | NumberStyles.AllowDecimalPoint);

问题是,DecimalConverter.ConvertFrom 在调用 Number.Parse 时不支持 NumberStyles 枚举的 AllowThousands 标志。好消息是,有一种方法可以 "teach" 做到这一点!

Decimal.Parse 内部调用 Number.Parse 并将数字样式设置为 Number,为此 AllowThousands 标志设置为 true。

[__DynamicallyInvokable]
public static decimal Parse(string s)
{
    return Number.ParseDecimal(s, NumberStyles.Number, NumberFormatInfo.CurrentInfo);
}

当您从描述符接收类型转换器时,您实际上得到了 DecimalConverter 的一个实例。 ConvertFrom方法比较笼统,比较大,所以我这里只引用当前场景的相关部分。缺少的部分是实现对十六进制字符串和异常处理的支持。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 也覆盖了 FromString 实现,问题出现了:

internal override object FromString(string value, NumberFormatInfo formatInfo) 
{
    return Decimal.Parse(value, NumberStyles.Float, formatInfo);
}

数字样式设置为FloatAllowThousands标志设置为false!但是,您可以使用几行代码编写自定义转换器来修复此问题。

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

1请注意,代码看起来与原始实现类似。如果您需要 "unquoted" 东西,请直接将其委托给 base 或自行实现。您可以使用 ILSpy/DotPeek/etc 查看实现。或者通过 Visual Studio.

调试它们

最后,在 Reflection 的帮助下,您可以为 Decimal 设置类型转换器以使用您的新自定义类型转换器!

TypeDescriptor.AddAttributes(typeof(decimal), new TypeConverterAttribute(typeof(NumericDecimalConverter)));