12

I have an ASP.NET MVC 5 project with Fluent Validation for MVC 5. I am also using a jQuery masking plugin to automatically add thousands to double values.

In the model I have:

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

In the view I have:

<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>

A fluent validator is defined for the containing model but it contains no rules. I am using server side validation only.

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

    }
}

Unfortunately I get a validation error for turnover as follows: enter image description here

I have tried using Model Binding to solve this problem. But the break point in model binder never gets hit - fluent validation seems to block the value from reaching the model binder.

gls123
  • 5,467
  • 2
  • 28
  • 28
  • 1
    Have you considered changing the type to string and then using a backing field that is a double, then having the getter and setter do the coversion for you? I feel like there is a better way to handle it like you would using a converter in XAML apps, but I'm not sure how to do it in this context... or float something off of this: http://stackoverflow.com/questions/29975128/asp-net-mvc-data-annotation-for-currency-format – Sinaesthetic Nov 01 '16 at 17:53
  • Where is your FluentValidation for your model? Is the error from `ModelState`, or does your AbstractValidator cause the error? – krillgar Nov 01 '16 at 18:18
  • I've added the code for the empty validator – gls123 Nov 01 '16 at 23:26
  • 1
    When you say _I am using server side validation only_ - have you actually disabled client side validation? If you have not specifically disabled it, then `jquery.validate.js` would prevent the for being submitted. –  Nov 09 '16 at 05:49
  • `@Html.ValidationMessageFor` looks suspicious - why you're using client-side validation besides server-side validation? The `Turnover` property declared as `Nullable` which accepts number value without thousand separator, you need to set client-side formatting without altering field value using JS or remove client-side validation for corresponding property. – Tetsuya Yamamoto Nov 09 '16 at 06:16
  • Might be culture related issue. What is your Thread Culture? Also verify the decimal separator of your culture is `,` - `Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator` – Developer Nov 12 '16 at 17:54

3 Answers3

5

Few things to mention:

  • The issue has nothing in common with Fluent Validation. I was able to reproduce/fix it with or without Fluent Validation.
  • The DataFormatString used is incorrect (missing the value placeholder). It should really be "{0:#,##0}".
  • The ModelBinder approach from the link actually works. I guess you forgot that it is written for decimal data type, while your model is using double?, so you have to write and register another one for double and double? types.

Now on the subject. There are actually two solutions. Both of them use the following helper class for the actual string conversion:

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

Custom IModelBinder

This is a modified version of the linked approach. It's a single class that handles all the numeric types and their respective nullable types:

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;
    }
}

All you need is to register it in your Application_Start:

protected void Application_Start()
{
    NumericValueBinder.Register();  
    // ...
}

Custom TypeConverter

This is not specific to ASP.NET MVC 5, but DefaultModelBinder delegates string conversion to the associated TypeConverter (similar to other NET UI frameworks). In fact the issue is caused by the fact that the default TypeConverter classes for numeric types do not use Convert class, but Parse overloads with NumberStyles passing NumberStyles.Float which excludes NumberStyles.AllowThousands.

Fortunately System.ComponentModel provides extensible Type Descriptor Architecture which allows you to associate a custom TypeConverter. The plumbing part is a bit complicated (you have to register a custom TypeDescriptionProvider in order to provide ICustomTypeDescriptor implementation that finally returns custom TypeConverter), but with the help of the provided base classes that delegate most of the stuff to the underlying object, the implementation looks like this:

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

(Yeah, a lot of boilerplate code in order to add one essential line! From the other side, there is no need to handle nullable types because DefaultModelBinder already does that :)

Similar to the first approach, all you need is to register it:

protected void Application_Start()
{
    NumericTypeDescriptionProvider.Register();  
    // ...
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • I tried all these techniques and could not get any to work with FluentValidation. But for the detail I think half the bounty is appropriate. I think FluentValidation needs changing with a pull request. – gls123 Nov 17 '16 at 11:47
  • No problem, I don't care for bounties. But really sad to hear the issue is not resolved. I installed FluentValidation package and set it up, didn't see any difference in that regard. W/o the above hooks, the issue is duplicated. With them - fixed. May be there is something else involved - for instance, if I use `EditorFor`, the above fixes does not work for `int` and similar (but work for `decimal`, `double` and `float`). But `TextBoxFor` works for all types. May be that's a clue, I don't know. Will be watching, please post if you find the cause/solution. Good luck! – Ivan Stoev Nov 17 '16 at 11:55
  • Thanks - that might be it - need to switch EditorFor to TextBoxFor for. – gls123 Nov 17 '16 at 13:27
  • I now have this working. I owe you 125 points for the bounty but there does not appear to be a way for me to give this to you ... – gls123 Nov 28 '16 at 11:51
4

The problem is not with FluentValidation, but MVC's model binding to double type. MVC's default model binder cannot parse the number and assigns false to IsValid.

The problem was solved after I included the following code, 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);
    }
}

And include the lines below in Application_Start:

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

Also consider explicitly stating the current culture as in this post.

Community
  • 1
  • 1
Gokhan Kurt
  • 8,239
  • 1
  • 27
  • 51
1

It can be a culture issue. Try use dot instead of a comma on a client side (10,000,000 -> 10.000.000) or fix culture issue on server side.

Alex Erygin
  • 3,161
  • 1
  • 22
  • 22