【问题标题】:Fluent Validation not accepting numbers with thousands separatorFluent Validation 不接受带有千位分隔符的数字
【发布时间】:2016-11-01 17:45:51
【问题描述】:

我有一个 ASP.NET MVC 5 项目,带有 MVC 5 的 Fluent Validation。我还使用 jQuery 掩码插件自动将数千个值添加到 double 值。

在我的模型中:

    [Display(Name = "Turnover")]
    [DisplayFormat(ApplyFormatInEditMode = true,ConvertEmptyStringToNull =true,DataFormatString ="#,##0")]
    public double? Turnover { get; set; }

在我看来:

<th class="col-xs-2">
    @Html.DisplayNameFor(model=>model.Turnover)
</th>
<td class="col-xs-4">
    @Html.TextBoxFor(model => model.Turnover, new { @class = "form-control number", placeholder="Enter number. Thousands added automatically" })
</td>
<td class="col-xs-6">
    @Html.ValidationMessageFor(model => model.Turnover, "", new { @class = "text-danger" })
</td>

为包含模型定义了一个流畅的验证器,但它不包含任何规则。我只使用服务器端验证。

public class MyModelValidator: AbstractValidator<MyModel>
{
    public MyModelValidator()
    {

    }
}

不幸的是,我收到以下营业额验证错误:

我已经尝试使用Model Binding 来解决这个问题。但是模型绑定器中的断点永远不会被击中-流畅的验证似乎阻止了该值到达模型绑定器。

【问题讨论】:

  • 您是否考虑过将类型更改为字符串,然后使用双精度支持字段,然后让 getter 和 setter 为您做覆盖?我觉得有一种更好的方法来处理它,就像您在 XAML 应用程序中使用转换器一样,但我不确定如何在这种情况下做到这一点......或者从中浮出一些东西:stackoverflow.com/questions/29975128/…
  • 您的模型的 FluentValidation 在哪里?是来自ModelState 的错误,还是您的 AbstractValidator 导致了错误?
  • 我已经添加了空验证器的代码
  • 当你说我只使用服务器端验证 - 你真的禁用了客户端验证吗?如果您没有特别禁用它,那么jquery.validate.js 将阻止提交。
  • @Html.ValidationMessageFor 看起来很可疑 - 为什么除了服务器端验证之外还使用客户端验证? Turnover 属性声明为 Nullable&lt;double&gt; 接受不带千位分隔符的数值,您需要设置客户端格式而不使用 JS 更改字段值或删除相应属性的客户端验证。

标签: c# asp.net-mvc fluentvalidation


【解决方案1】:

有几件事要提:

  • 该问题与 Fluent Validation 没有任何共同之处。无论是否使用 Fluent Validation,我都能重现/修复它。
  • 使用的DataFormatString 不正确(缺少值占位符)。应该是"{0:#,##0}"
  • link 中的 ModelBinder 方法确实有效。我猜你忘了它是为decimal 数据类型编写的,而你的模型使用double?,所以你必须为doubledouble? 类型编写并注册另一个。

现在谈这个话题。实际上有两种解决方案。它们都使用以下帮助类进行实际的字符串转换:

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 以提供最终返回自定义 TypeConverterICustomTypeDescriptor 实现),但借助提供的基类,这些基类将大部分内容委托给底层对象,实现如下所示:

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();  
    // ...
}

【讨论】:

  • 我尝试了所有这些技术,但无法使用 FluentValidation。但是对于细节,我认为一半的赏金是合适的。我认为 FluentValidation 需要根据拉取请求进行更改。
  • 没问题,我不在乎赏金。但是听到这个问题没有解决真的很难过。我安装了 FluentValidation 包并进行了设置,在这方面没有发现任何区别。没有上面的钩子,问题是重复的。和他们一起 - 固定。可能还涉及其他问题 - 例如,如果我使用EditorFor,上述修复不适用于int 和类似的(但适用于decimaldoublefloat)。但是TextBoxFor 适用于所有类型。这可能是一个线索,我不知道。将关注,如果您找到原因/解决方案,请发布。祝你好运!
  • 谢谢 - 可能是这样 - 需要将 EditorFor 切换到 TextBoxFor。
  • 我现在有这个工作。我欠你 125 分的赏金,但我似乎没有办法把这个给你...
【解决方案2】:

问题不在于 FluentValidation,而在于 MVC 的模型绑定到 double 类型。 MVC 的默认模型绑定器无法解析数字并将false 分配给IsValid

在我包含以下代码后,问题得到解决,credits to this post

public class DoubleModelBinder : System.Web.Mvc.DefaultModelBinder {
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
        var result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (result != null && !string.IsNullOrEmpty(result.AttemptedValue)
            && (bindingContext.ModelType == typeof(double) || bindingContext.ModelType == typeof(double?))) {
            double temp;
            if (double.TryParse(result.AttemptedValue, out temp)) return temp;
        }
        return base.BindModel(controllerContext, bindingContext);
    }
}

并在Application_Start中包含以下行:

ModelBinders.Binders.Add(typeof(double), new DoubleModelBinder());
ModelBinders.Binders.Add(typeof(double?), new DoubleModelBinder());

还可以考虑在this post 中明确说明当前文化。

【讨论】:

  • 有效但不适用于整数
  • @gls123 呵呵,您必须为intint? 编写另一个类并将其添加到模型绑定器中。
【解决方案3】:

这可能是一个文化问题。尝试在客户端使用点而不是逗号(10,000,000 -> 10.000.000)或在服务器端修复文化问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-11-10
    • 2018-05-11
    • 1970-01-01
    • 2012-03-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-21
    相关资源
    最近更新 更多