Search code examples
c#asp.net-mvcvalidationglobalization

MVC 5.2.3 - Globalization - DateTime & Decimal with Range


Long story short, I started a new project using MVC 5.2.3 (with all updated JS frameworks) expecting that many of the issues I had with validations from MVC 2 were solved, how wrong was I.

Basically I am driving myself crazy trying to provide validation to DateTime and Decimal.
With the decimal field, using browsers in EN and DE (cultures) I am getting problems with comma and point (decimal division) and with the range that I am setting.

With the DateTime I even tried a DisplayFormat just to display the Date and not the time, and anyway the order of day/month/year with separations with . or / or - are simply failing.

Examples:

  • In DE - Type: 999,999 --> Result: ERROR "Das Feld "D1 DE" muss zwischen 0 und 999,9999 liegen. " - should be accepted.
  • In EN - Type: 999.999 --> Result: is accepted as it should be (correct)

The same applies to the datetime display format and similar JS validations....

I share with you guys what I have at the moment:

TestObject

public class TestObject
{       
    ....
    [Display(Name = "D2", ResourceType = typeof(WebApplication1.Models.Res.TestObject))]
    [Range(0, 999.9999)]
    public decimal? D1 { get; set; }
    // On DBContext I have defined the range/precision
    // modelBuilder.Entity<TestObject>().Property(x => x.D2).HasPrecision(7, 4);

    [Display(Name = "Date1", ResourceType = typeof(WebApplication1.Models.Res.TestObject))]
    //[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = @"{0:dd\/MM\/yyyy}")]
    public DateTime Date1 { get; set; }
    ....
}

view.cshtml

<div class="form-group">
            @Html.LabelFor(model => model.D2, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.D2, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.D2, "", new { @class = "text-danger" })
            </div>
        </div>
 <div class="form-group">
            @Html.LabelFor(model => model.Date1, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Date1, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Date1, "", new { @class = "text-danger" })
            </div>
        </div>

_layout.cshtml

    <script src="~/Scripts/globalize/globalize.js" type="text/javascript"></script></script>
    <script src="~/Scripts/globalize/cultures/globalize.culture.de.js" type="text/javascript"></script>
    <script src="~/Scripts/globalize/cultures/globalize.culture.en-US.js" type="text/javascript"></script>  

    <script>
  $.validator.methods.number = function (value, element) {
    return this.optional(element) ||
        !isNaN(Globalize.parseFloat(value));
  }

  $.validator.methods.date = function (value, element) {
    return this.optional(element) ||
        Globalize.parseDate(value);
  }

  $(document).ready(function () {
    Globalize.culture('@System.Threading.Thread.CurrentThread.CurrentCulture');
  });
</script>

DecimalModelBinder

public class DecimalModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext,
                                 ModelBindingContext bindingContext)
{
    object result = null;

    // Don't do this here!
    // It might do bindingContext.ModelState.AddModelError
    // and there is no RemoveModelError!
    // 
    // result = base.BindModel(controllerContext, bindingContext);

    string modelName = bindingContext.ModelName;
    string attemptedValue =
        bindingContext.ValueProvider.GetValue(modelName).AttemptedValue;

    // Depending on CultureInfo, the NumberDecimalSeparator can be "," or "."
    // Both "." and "," should be accepted, but aren't.
    string wantedSeperator = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
    string alternateSeperator = (wantedSeperator == "," ? "." : ",");

    if (attemptedValue.IndexOf(wantedSeperator) == -1
        && attemptedValue.IndexOf(alternateSeperator) != -1)
    {
        attemptedValue =
            attemptedValue.Replace(alternateSeperator, wantedSeperator);
    }

    try
    {
        if (bindingContext.ModelMetadata.IsNullableValueType
            && string.IsNullOrWhiteSpace(attemptedValue))
        {
            return null;
        }

        result = decimal.Parse(attemptedValue, NumberStyles.Any);
    }
    catch (FormatException e)
    {
        bindingContext.ModelState.AddModelError(modelName, e);
    }

    return result;
}

}

Global.asax

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        ...
        ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
        ModelBinders.Binders.Add(typeof(decimal?), new DecimalModelBinder());
       ...
    }

    private void Application_BeginRequest(Object source, EventArgs e)
    {
        HttpApplication application = (HttpApplication)source;
        HttpContext context = application.Context;

        string culture = null;
        if (context.Request.UserLanguages != null && Request.UserLanguages.Length > 0)
        {
            culture = Request.UserLanguages[0];
            Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(culture);
            Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
        }
    }
}

web.config

    <globalization culture="auto" uiCulture="auto" enableClientBasedCulture="true" />

Solution

  • It seems that I also needed to create my own range method..... Now it seems to be working.

    $.validator.methods.range = function(value, element, param) {
        console.log('----------------');
        console.log(value);
        console.log(element);                
        var globalizedValue = value.replace(",", ".");
        console.log(param[0]);
        var globalizedValueParam0 = param[0];
        console.log(param[1]);
        var globalizedValueParam1 = param[1];        
        console.log(globalizedValueParam0);
        console.log(globalizedValueParam1);
    
        //return this.optional(element) || (globalizedValue >= param[0] && globalizedValue <= param[1]);
        return this.optional(element) || (globalizedValue >= globalizedValueParam0 && globalizedValue <= globalizedValueParam1);
    };