Десятичное значение привязки 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);