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:
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.
FluentValidation provides a great alternative to Data Annotations in order to validate models. It gives better control of validation rules and makes validation rules easy to read, easy to test, and enable great separation of concerns.
FluentValidation is a . NET library for building strongly-typed validation rules. FluentValidation 11 supports the following platforms: . NET Core 3.1.
Fluent API is an advanced way of specifying model configuration that covers everything that data annotations can do in addition to some more advanced configuration not possible with data annotations.
FluentValidation is a server-side library and does not provide any client-side validation directly. However, it can provide metadata which can be applied to the generated HTML elements for use with a client-side framework such as jQuery Validate in the same way that ASP. NET's default validation attributes work.
Few things to mention:
DataFormatString
used is incorrect (missing the value placeholder). It should really be "{0:#,##0}"
.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();
// ...
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With