Fluent Validation не принимает числа с разделителем тысяч

У меня есть проект ASP.NET MVC 5 с Fluent Validation для MVC 5. Я также использую плагин маскирования jQuery для автоматического добавления тысяч в двойные значения.

В модели у меня есть:

    [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()
    {

    }
}

К сожалению, я получаю ошибку проверки для оборота следующим образом: введите описание изображения здесь

Я попытался использовать привязку к тексту для решения этой проблемы. Но точка разрыва в привязке к модели никогда не попадает в глаза - текущая проверка, похоже, блокирует значение от достижения связующего.

Ответ 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();  
    // ...
}

Ответ 2

Проблема заключается не в FluentValidation, а в привязке модели MVC к типу double. Привязка по умолчанию для MVC не может анализировать номер и присваивает false IsValid.

Проблема была решена после того, как я включил следующий код, кредиты на этот пост.

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

Также рассмотрите явное указание текущей культуры, как в этом сообщении.

Ответ 3

Это может быть проблема культуры. Попробуйте использовать точку вместо запятой на стороне клиента (10 000 000 → 10.000.000) или исправить проблему культуры на стороне сервера.