有几件事要提:
- 该问题与 Fluent Validation 没有任何共同之处。无论是否使用 Fluent Validation,我都能重现/修复它。
- 使用的
DataFormatString 不正确(缺少值占位符)。应该是"{0:#,##0}"。
-
link 中的
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 以提供最终返回自定义 TypeConverter 的 ICustomTypeDescriptor 实现),但借助提供的基类,这些基类将大部分内容委托给底层对象,实现如下所示:
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();
// ...
}