We ran into an interesting problem in Chrome using MVC, dates, and EditorFor. When we tried to submit a search, the dates kept getting the wrong format applied to them once the page loaded again, and because of this Chrome wouldn't display a datepicker for them anymore, even though it did when the page first loaded. We eventually discovered that this was due to the way MVC operates with query string and model values, and we were able to find a couple solutions for it. Come along we me and let's do a bug hunt!
Image by AZRainMan
Found it!
The Problem
We needed to search a set of data using a date range, and were trying to set it up using the POST-REDIRECT-GET pattern, which specifies that a POST action should only ever return a REDIRECT. But what were noticing was that, in Chrome, the text box inputs for the dates were getting the wrong format!
Our application guidelines specify to use the browser-implemented datepickers wherever possible. Chrome only accepts the ISO standard yyyy-MM-dd format for use in its datepicker, so we were setting up our view model to handle that format using DisplayFormatAttribute and DataTypeAttribute
Here's our setup, simplified for your viewing pleasure:
ViewModels/Home/SearchVM.cs
public class SearchVM
{
[DisplayName("Start: ")]
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime StartDate { get; set; }
[DisplayName("End: ")]
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EndDate { get; set; }
}
Controllers/HomeController.cs
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index(DateTime? startDate, DateTime? endDate)
{
SearchVM model = new SearchVM()
{
StartDate = startDate ?? DateTime.Now.AddMonths(-6),
EndDate = endDate ?? DateTime.Now
};
return View(model);
}
[HttpPost]
public ActionResult Index(SearchVM model)
{
return RedirectToAction("Index", new { startDate = model.StartDate, endDate = model.EndDate });
}
}
Views/Home/Index.cshtml
@model DatesMappingDemo.ViewModels.Home.SearchVM
@{
ViewBag.Title = "Home Page";
}
@using(Html.BeginForm())
{
<div>
<div>
@Html.LabelFor(x => x.StartDate)
@Html.EditorFor(x => x.StartDate)
</div>
<div>
@Html.LabelFor(x => x.EndDate)
@Html.EditorFor(x => x.EndDate)
</div>
<div>
<input type="submit" value="Go!" />
</div>
</div>
}
When we first arrived on the page, it looked like this:
All the markup looks good, the inputs are in the correct format, everything looks great. When we submitted a search, though, the resulting page looked like this:
What the hell?! The dates are in the wrong format! But we specified DisplayFormat on each property, so why are they showing up as the wrong formats?
It turns our that the EditorFor call was looking at ModelState to get the values of StartDate and EndDate, and having those names appear in the query string causes them to be added to the ModelState. The HtmlHelpers will check ModelState before they check model values, so if a value exists in the ModelState with the name that the helpers are looking for, that value gets displayed.
(As an aside, while this definitely seems like a bug to me, I really don't know how to fix it. It was noticed as far back as MVC 1 and the behavior still exists. Granted, it's not that big a deal to implement the solutions below, but it took us a little while to understand why MVC was behaving this way.)
Two Solutions
So how do we fix this? One way (the nuclear option, really) is supposedly to reset the ModelState during the GET action:
[HttpGet]
public ActionResult Index(DateTime? startDate, DateTime? endDate)
{
SearchVM model = new SearchVM()
{
StartDate = startDate ?? DateTime.Now.AddMonths(-6),
EndDate = endDate ?? DateTime.Now
};
ModelState.Clear(); //Reset the model state
return View(model);
}
Unfortunately that didn't work for us, it would always display the default values. Instead, we went the simpler route and just renamed our action parameters:
[HttpGet]
public ActionResult Index(DateTime? start, DateTime? end)
{
SearchVM model = new SearchVM()
{
StartDate = start ?? DateTime.Now.AddMonths(-6),
EndDate = end ?? DateTime.Now
};
return View(model);
}
[HttpPost]
public ActionResult Index(SearchVM model)
{
return RedirectToAction("Index", new { start = model.StartDate, end = model.EndDate });
}
Now the search works perfectly!
Took us a little while to figure this out, but now that we've got it, we'll keep an eye on our names in models and the query string.
Happy Coding!