Custom Date Formats and the MVC Model Binder

Let’s say that you have a model with a DateTime property.

public class Person
{
  //...
  public DateTime DateOfBirth { get; set; }
}

Suppose that you want someone to fill out the details and post them back to your site. Simple enough: add a couple of action methods and then insert an EditorFor in your view:

//controller
public ActionResult EditDetails()
{
  return View(new Person()); //or loaded from DB, etc
}

[HttpPost]
public ActionResult SaveDetails(Person person)
{
  //save updated person to DB
}
<!-- view -->
<div class="editor-field">
  @Html.EditorFor(model => model.DateOfBirth)
</div>

That’s all you need for basic date editing, though for your users’ sake I suggest you look at a nicer editor than the plain input that this will generate!

But what if you want to support some format besides the default? MVC doesn’t handle that out-of-the-box so you’ll need a custom model binder…

Custom Model Binder

The model binder is responsible for converting a series of form values into the rich, strongly-typed model that is passed as a parameter to your action method.

Thankfully there’s no need to re-implement all of the pretty-complex functionality involved in that process just to alter how dates are parsed; a custom model binder can be associated with a single type, and the rest of the binding functionality will work as normal.

Custom model binder implementations need to implement the IModelBinder interface, but the easier approach is to inherit from System.Web.Mvc.DefaultModelBinder and override the one method in which we are interested: BindModel

public class DateTimeModelBinder : DefaultModelBinder
{
  public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
  {
    // ???
  }
}

The BindModel method is responsible for returning the value of a model based on the controller- and the binding-context (which between them provide a lot of contextual information about the request), and our custom date parsing implementation will need to:

  • get hold of the custom format that we want to use
  • use that format with the value that has been posted back to the server

Specifying the Date Format

There are a lot of different ways in which you might determine or specify the custom format, but for the purposes of this example I am just going to pass it into the constructor.

public class DateTimeModelBinder : DefaultModelBinder
{
  private string _customFormat;

  public DateTimeModelBinder(string customFormat)
  {
    _customFormat = customFormat;
  }

  public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
  {
    // ???
  }
}

In a real implementation you might grab this from a data annotation attribute, from configuration, from the request information - wherever makes sense for your project.

Getting the POSTed Value

The bindingContext that is passed to the BindModel method gives us a couple of properties that we can use to get the POSTed value from the request:

  • ModelName gives us the name of the property on which the binder is being used
  • ValueProvider.GetValue(string) will return an instance of ValueProviderResult from which we can get the raw value

The ValueProvider.GetValue method takes a “key” as the parameter, for which we can use the value of the ModelName property:

var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
value.AttemptedValue; //provides the raw value

From here it is a simple step to tie everything together with a DateTime.ParseExact:

public class DateTimeModelBinder : DefaultModelBinder
{
  private string _customFormat;

  public DateTimeModelBinder(string customFormat)
  {
    _customFormat = customFormat;
  }

  public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
  {
    var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
    return DateTime.ParseExact(value.AttemptedValue, _customFormat, CultureInfo.InvariantCulture);
  }
}

Hooking it up

Now that we have the model binder, we need to associate it with all DateTime properties throughout our application. We do this by adding an instance of the binder to the ModelBinders.Binders collection from within Global.asax.cs.

protected void Application_Start()
{
  //...
  var binder = new DateTimeModelBinder(GetCustomDateFormat());
  ModelBinders.Binders.Add(typeof(DateTime), binder);
  ModelBinders.Binders.Add(typeof(DateTime?), binder);
}

This will associate our new binder with any properties or values of type DateTime or DateTime?. Now, whenever one of these types is encountered by MVC whilst trying to parse a POSTed value it will use our custom binder and therefore our custom format!