Десятичное значение привязки ASP.NET MVC

Я пытаюсь понять, почему среда отказывается привязывать значение "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. Он использует System.ComponentModel.DecimalConverter.

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

Здесь сообщение "1,234.0000 не является допустимым значением для десятичного числа". происходит от.

Я попытался запустить следующий код на своей игровой площадке:

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 правильно анализирует одну и ту же строку.

Ответы

Ответ 1

Проблема заключается в том, что DecimalConverter.ConvertFrom не поддерживает флаг AllowThousands "nofollow noreferrer" > NumberStyles, когда он вызывает Number.Parse. Хорошая новость заключается в том, что существует способ "научить" это делать!

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

С типом номера, установленным на Float, флаг AllowThousands установлен в значение 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 Обратите внимание, что код похож на исходную реализацию. Если вам нужен "неуказанный" материал, он делегирует его непосредственно на base или реализует его самостоятельно. Вы можете просмотреть реализацию, используя ILSpy/DotPeek/etc. или путем отладки в них из Visual Studio.

Наконец, с небольшой помощью Reflection вы можете установить конвертер типов для Decimal, чтобы использовать новый пользовательский!

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

Ответ 2

На основе комментария из статьи о привязке десятичной модели Филом Хааком здесь, я считаю частью ответа на вопрос "почему" заключается в том, что культура в браузерах сложна, и вам не может быть гарантировано, что ваша культура приложения будет тем же самым параметром культуры, который используется пользователем/браузером для десятичных знаков. В любом случае это известный "вопрос", и подобные вопросы были заданы ранее с помощью множества предлагаемых решений, в дополнение к так: Принять запятую и точку в виде десятичного разделителя и Как установить десятичные разделители в ASP.NET MVC-контроллерах?, например.

Ответ 3

Вы можете попробовать переопределить DefaultModelBinder. Дайте мне знать, если это не сработает, и я удалю этот пост. Я фактически не собирал приложение 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);
        }
    }
}

Ответ 4

Проблема здесь выглядит по умолчанию Число стилей, примененная к Decimal.Parse(string).

Из документации MSDN

Оставшиеся отдельные флаги полей определяют элементы стиля, которые могут быть, но не обязательно должны присутствовать в строчном представлении десятичного числа для успешной операции синтаксического анализа.

Итак, это означает, что как d1, так и d2 ниже успешно разбираются

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

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

Однако при применении преобразователя типов кажется, что это, по существу, позволяет только разрешать учебные пространства, допускать десятичную точку и допускать ведущий знак. Таким образом, приведенный ниже d3 будет вызывать ошибку времени выполнения

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