19

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
  • 5,052
  • 5
  • 34
  • 44
  • 1
    Use one format for all requests. http://stackoverflow.com/a/28219557/960997 – rnofenko Jan 29 '15 at 16:28
  • @fomaa I now use datepicker with the [altField](http://api.jqueryui.com/datepicker/#option-altField) and [altFormat](http://api.jqueryui.com/datepicker/#option-altFormat) options to supply a hiddenfield with a culture invariant version of the date (like ISO8601 as you mention). Then submitting that field instead, i feel this is a better solution. – Jim Wolff Jan 30 '15 at 07:39

7 Answers7

21

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.

gdoron
  • 147,333
  • 58
  • 291
  • 367
12

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);
}
Iridio
  • 9,213
  • 4
  • 49
  • 71
10

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;
        }
    }
    }
    
Community
  • 1
  • 1
Rob Kent
  • 5,183
  • 4
  • 33
  • 54
  • 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. – vbocan May 16 '14 at 13:07
3

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.

Digbyswift
  • 10,310
  • 4
  • 38
  • 66
  • @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. – Jim Wolff Oct 25 '11 at 12:20
  • 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. – Digbyswift Oct 25 '11 at 12:26
1

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);
    }
}
Jeff Dunlop
  • 893
  • 1
  • 7
  • 20
0

that did the trick for me

    <system.web>     
       <globalization enableClientBasedCulture="true" uiCulture="Auto" culture="Auto" />
    </system.web>
Otto Kanellis
  • 3,629
  • 1
  • 23
  • 24
  • 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 – Jim Wolff Oct 29 '12 at 14:41
0

I have a updated solution for MVC5 based on the Post of @gdoron. I will share it in case anyone else is looking for this. The class inherits from DefaultModelBinder and has exception handling for invalid dates. It also can handle null values:

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

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

        // in datetime? binding attemptedValue can be Null
        if (attemptedValue != null && !string.IsNullOrWhiteSpace(attemptedValue))
        {
            try
            {
                var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
                result = DateTime.Parse(value.AttemptedValue, CultureInfo.CurrentCulture);
            }
            catch (FormatException e)
            {
                bindingContext.ModelState.AddModelError(modelName, e);
            }
        }

        return result;
    }
}

And just like the mentioned sample in the Global.Asax write

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

Stackberg
  • 300
  • 3
  • 12