ASP.NET MVC datetime culture issue when passing value back to controller

27,284

Solution 1

You can create a Binder extension to handle the date in the culture format.

This is a sample I wrote to handle the same problem with Decimal type, hope you get the idea

 public class DecimalModelBinder : IModelBinder
 {
   public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   {
     ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
     ModelState modelState = new ModelState { Value = valueResult };
     object actualValue = null;
     try
     {
       actualValue = Convert.ToDecimal(valueResult.AttemptedValue, CultureInfo.CurrentCulture);
     }
     catch (FormatException e)
     {
       modelState.Errors.Add(e);
     }

     bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
     return actualValue;
  }
}

Update

To use it simply declare the binder in Global.asax like this

protected void Application_Start()
{
  AreaRegistration.RegisterAllAreas();
  RegisterGlobalFilters(GlobalFilters.Filters);
  RegisterRoutes(RouteTable.Routes);

  //HERE you tell the framework how to handle decimal values
  ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());

  DependencyResolver.SetResolver(new ETAutofacDependencyResolver());
}

Then when the modelbinder has to do some work, it will know automatically what to do. For example, this is an action with a model containing some properties of type decimal. I simply do nothing

[HttpPost]
public ActionResult Edit(int id, MyViewModel viewModel)
{
  if (ModelState.IsValid)
  {
    try
    {
      var model = new MyDomainModelEntity();
      model.DecimalValue = viewModel.DecimalValue;
      repository.Save(model);
      return RedirectToAction("Index");
    }
    catch (RulesException ex)
    {
      ex.CopyTo(ModelState);
    }
    catch
    {
      ModelState.AddModelError("", "My generic error message");
    }
  }
  return View(model);
}

Solution 2

you can change the default model binder to use the user culture using IModelBinder

   public class DateTimeBinder : IModelBinder
   {
       public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
       {
           var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
           var date = value.ConvertTo(typeof(DateTime), CultureInfo.CurrentCulture);

           return date;    
       }
   }

And in the Global.Asax write:

ModelBinders.Binders.Add(typeof(DateTime), new DateTimeBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeBinder());

Read more at this excellent blog that describe why Mvc framework team implemented a default Culture to all users.

Solution 3

This issue arises because you are using the GET method on your Form. The QueryString Value Provider in MVC always uses Invariant/US date format. See: MVC DateTime binding with incorrect date format

There are three solutions:

  1. Change your method to POST.
  2. As someone else says, change the date format to ISO 8601 "yyyy-mm-dd" before submission.
  3. Use a custom binder to always treat Query String dates as GB. If you do this you have to make sure that all dates are in that form:

    public class UKDateTimeModelBinder : IModelBinder
    {
    private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
    
    /// <summary>
    /// Fixes date parsing issue when using GET method. Modified from the answer given here:
    /// https://stackoverflow.com/questions/528545/mvc-datetime-binding-with-incorrect-date-format
    /// </summary>
    /// <param name="controllerContext">The controller context.</param>
    /// <param name="bindingContext">The binding context.</param>
    /// <returns>
    /// The converted bound value or null if the raw value is null or empty or cannot be parsed.
    /// </returns>
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var vpr = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
    
        if (vpr == null)
        {
            return null;
    
        }
    
        var date = vpr.AttemptedValue;
    
        if (String.IsNullOrEmpty(date))
        {
            return null;
        }
    
        logger.DebugFormat("Parsing bound date '{0}' as UK format.", date);
    
        // Set the ModelState to the first attempted value before we have converted the date. This is to ensure that the ModelState has
        // a value. When we have converted it, we will override it with a full universal date.
        bindingContext.ModelState.SetModelValue(bindingContext.ModelName, bindingContext.ValueProvider.GetValue(bindingContext.ModelName));
    
        try
        {
            var realDate = DateTime.Parse(date, System.Globalization.CultureInfo.GetCultureInfoByIetfLanguageTag("en-GB"));
    
            // Now set the ModelState value to a full value so that it can always be parsed using InvarianCulture, which is the
            // default for QueryStringValueProvider.
            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, new ValueProviderResult(date, realDate.ToString("yyyy-MM-dd hh:mm:ss"), System.Globalization.CultureInfo.GetCultureInfoByIetfLanguageTag("en-GB")));
    
            return realDate;
        }
        catch (Exception)
        {
            logger.ErrorFormat("Error parsing bound date '{0}' as UK format.", date);
    
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, String.Format("\"{0}\" is invalid.", bindingContext.ModelName));
            return null;
        }
    }
    }
    

Solution 4

When submitting a date you should always try and submit it in the format "yyyy-MM-dd". This will allow for it to become culture independent.

I normally have a hidden field which maintains the date in this format. This is relatively simple using jQuery UI's datepicker.

Solution 5

Why not simply inspect the culture of the data and convert it as such? This simple approach allowed me to use strongly typed dates in models, show action links and edit fields in the desired locale and not have to fuss at all binding it back into a strongly typed DateTime:

public class DateTimeBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        return value.ConvertTo(typeof(DateTime), value.Culture);
    }
}
Share:
27,284
Jim Wolff
Author by

Jim Wolff

Updated on July 05, 2022

Comments

  • Jim Wolff
    Jim Wolff almost 2 years

    How can i tell my controller/model what kind of culture it should expect for parsing a datetime?

    I was using some of this post to implement jquery datepicker into my mvc application.

    When i submit the date it gets "lost in translation" i'm not using the US formatting for the date, so when it gets sent to my controller it simply becomes null.

    I have a form where the user chooses a date:

    @using (Html.BeginForm("List", "Meter", FormMethod.Get))
    {
        @Html.LabelFor(m => m.StartDate, "From:")
        <div>@Html.EditorFor(m => m.StartDate)</div>
    
        @Html.LabelFor(m => m.EndDate, "To:")
        <div>@Html.EditorFor(m => m.EndDate)</div>
    }
    

    I've made an edit template for this, to implement the jquery datepicker:

    @model DateTime
    @Html.TextBox("", Model.ToString("dd-MM-yyyy"), new { @class = "date" }) 
    

    I then create the datepicker widgets like this.

    $(document).ready(function () {
        $('.date').datepicker({ dateFormat: "dd-mm-yy" });
    });
    

    All this works fine.

    Here is where the problems start, this is my controller:

    [HttpGet]
    public ActionResult List(DateTime? startDate = null, DateTime? endDate = null)
    {
        //This is where startDate and endDate becomes null if the dates dont have the expected formatting.
    }
    

    This is why i would like to somehow tell my controller what culture it should expect? Is my model wrong? can i somehow tell it which culture to use, like with the data annotation attributes?

    public class MeterViewModel {
        [Required]
        public DateTime StartDate { get; set; }
        [Required]
        public DateTime EndDate { get; set; }
    }
    

    Edit: this link explains my issue and a very good solution to it aswell. Thanks to gdoron

  • Jim Wolff
    Jim Wolff over 12 years
    @Dibbyswift: I was thinking about the hidden field, but wasn't sure it was the way to go, since i dont want unnessesary hidden fields. But now with a second opinion i might go that direction.
  • Digbyswift
    Digbyswift over 12 years
    The advantage of a hidden field is that you can have a visible 'display' field that allows the users to supply a date in a user-friendly format, whilst the hidden field simply retains the value in the format you require.
  • Iridio
    Iridio over 12 years
    Updated the answer with an example. I hope it can help you better
  • mikek3332002
    mikek3332002 almost 12 years
    Will throw exception if it is an invalid date.
  • Jim Wolff
    Jim Wolff over 11 years
    Not sure my memory serves, but this does work if you use post, but not get. follow the link in one of the answers called: MVC DateTime binding with incorrect date format
  • Jeff Dunlop
    Jeff Dunlop over 10 years
    I tried this and it didn't work for GETs, e.g. from ActionLinks.
  • vbocan
    vbocan about 10 years
    ISO 8601 "yyyy-mm-dd" date format worked for me. I was struggling with a bilingual form and this is a good compromise. Thank you.