Ответ 1
Немного о чем:
- Проблема не имеет ничего общего с Fluent Validation. Я смог воспроизвести/исправить его с помощью Fluent Validation или без него.
- Используемый
DataFormatString
неверный (отсутствует заполнитель значений). Это действительно должно быть"{0:#,##0}"
. - Подход
ModelBinder
от ссылка действительно работает. Я думаю, вы забыли, что он написан для типа данныхdecimal
, в то время как ваша модель используетdouble?
, поэтому вам нужно написать и зарегистрировать другую для типовdouble
иdouble?
.
Теперь по теме. На самом деле существует два решения. Оба они используют следующий вспомогательный класс для фактического преобразования строк:
using System;
using System.Collections.Generic;
using System.Globalization;
public static class NumericValueParser
{
static readonly Dictionary<Type, Func<string, CultureInfo, object>> parsers = new Dictionary<Type, Func<string, CultureInfo, object>>
{
{ typeof(byte), (s, c) => byte.Parse(s, NumberStyles.Any, c) },
{ typeof(sbyte), (s, c) => sbyte.Parse(s, NumberStyles.Any, c) },
{ typeof(short), (s, c) => short.Parse(s, NumberStyles.Any, c) },
{ typeof(ushort), (s, c) => ushort.Parse(s, NumberStyles.Any, c) },
{ typeof(int), (s, c) => int.Parse(s, NumberStyles.Any, c) },
{ typeof(uint), (s, c) => uint.Parse(s, NumberStyles.Any, c) },
{ typeof(long), (s, c) => long.Parse(s, NumberStyles.Any, c) },
{ typeof(ulong), (s, c) => ulong.Parse(s, NumberStyles.Any, c) },
{ typeof(float), (s, c) => float.Parse(s, NumberStyles.Any, c) },
{ typeof(double), (s, c) => double.Parse(s, NumberStyles.Any, c) },
{ typeof(decimal), (s, c) => decimal.Parse(s, NumberStyles.Any, c) },
};
public static IEnumerable<Type> Types { get { return parsers.Keys; } }
public static object Parse(string value, Type type, CultureInfo culture)
{
return parsers[type](value, culture);
}
}
Пользовательский IModelBinder
Это модифицированная версия связанного подхода. Это один класс, который обрабатывает все числовые типы и их соответствующие типы с нулевым значением:
using System;
using System.Web.Mvc;
public class NumericValueBinder : IModelBinder
{
public static void Register()
{
var binder = new NumericValueBinder();
foreach (var type in NumericValueParser.Types)
{
// Register for both type and nullable type
ModelBinders.Binders.Add(type, binder);
ModelBinders.Binders.Add(typeof(Nullable<>).MakeGenericType(type), binder);
}
}
private NumericValueBinder() { }
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
if (!string.IsNullOrWhiteSpace(valueResult.AttemptedValue))
{
try
{
var type = bindingContext.ModelType;
var underlyingType = Nullable.GetUnderlyingType(type);
var valueType = underlyingType ?? type;
actualValue = NumericValueParser.Parse(valueResult.AttemptedValue, valueType, valueResult.Culture);
}
catch (Exception e)
{
modelState.Errors.Add(e);
}
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
Все, что вам нужно, это зарегистрировать его в Application_Start
:
protected void Application_Start()
{
NumericValueBinder.Register();
// ...
}
Пользовательский TypeConverter
Это не относится к ASP.NET MVC 5, но DefaultModelBinder
делегирует преобразование строки в связанный TypeConverter
(аналогично другие среды NET UI). На самом деле проблема связана с тем, что классы по умолчанию TypeConverter
для числовых типов не используют класс Convert
, но Parse
перегружает с помощью NumberStyles
прохождение NumberStyles.Float
, которое исключает NumberStyles.AllowThousands
.
К счастью, System.ComponentModel
предоставляет расширяемую Type Descriptor Architecture, которая позволяет связать пользовательский TypeConverter
. Водопроводная часть немного сложна (вам необходимо зарегистрировать пользовательский TypeDescriptionProvider
, чтобы обеспечить ICustomTypeDescriptor
, которая, наконец, возвращает пользовательский TypeConverter
), но с помощью предоставленных базовых классов, которые делегируют большую часть материала базовому объекту, реализация выглядит следующим образом:
using System;
using System.ComponentModel;
using System.Globalization;
class NumericTypeDescriptionProvider : TypeDescriptionProvider
{
public static void Register()
{
foreach (var type in NumericValueParser.Types)
TypeDescriptor.AddProvider(new NumericTypeDescriptionProvider(type, TypeDescriptor.GetProvider(type)), type);
}
readonly Descriptor descriptor;
private NumericTypeDescriptionProvider(Type type, TypeDescriptionProvider baseProvider)
: base(baseProvider)
{
descriptor = new Descriptor(type, baseProvider.GetTypeDescriptor(type));
}
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
return descriptor;
}
class Descriptor : CustomTypeDescriptor
{
readonly Converter converter;
public Descriptor(Type type, ICustomTypeDescriptor baseDescriptor)
: base(baseDescriptor)
{
converter = new Converter(type, baseDescriptor.GetConverter());
}
public override TypeConverter GetConverter()
{
return converter;
}
}
class Converter : TypeConverter
{
readonly Type type;
readonly TypeConverter baseConverter;
public Converter(Type type, TypeConverter baseConverter)
{
this.type = type;
this.baseConverter = baseConverter;
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return baseConverter.CanConvertTo(context, destinationType);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
return baseConverter.ConvertTo(context, culture, value, destinationType);
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return baseConverter.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string)
{
try { return NumericValueParser.Parse((string)value, type, culture); }
catch { }
}
return baseConverter.ConvertFrom(context, culture, value);
}
}
}
(Да, много кода шаблона, чтобы добавить одну важную строку! С другой стороны, нет необходимости обрабатывать типы с нулевым значением, потому что DefaultModelBinder
уже делает это:)
Как и в первом подходе, вам нужно всего лишь зарегистрировать его:
protected void Application_Start()
{
NumericTypeDescriptionProvider.Register();
// ...
}