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!

About these ads

11 thoughts on “Custom Date Formats and the MVC Model Binder

  1. Mark Van Mierlo says:

    Can you please document the GetCustomDateFormat() method.
    I don’t know how to implement it for my “nl-BE” locale, date format “dd/mm/yyyy”.
    Jquery reverses day and month when it puts it in my ASP.NET MVC-model.
    Thanks

    • The GetCustomDateFormat() method is expected to return a string similar to “en-US” or “nl-BE” – there is no complexity to it. I left it as a method because you could (in theory) get the date format from a number of places (e.g. config file, db, etc.)

      If the problem is in your client-side JavaScript then I would recommend avoiding the whole problem and using a library like momentjs to explicitly set the format. JavaScript dates are a notorious pain to work with and momentjs is an easy win

      • Mark Van Mierlo says:

        Thanks for the quick answer, Steve.

        I’m trying to match JQGRID to a MVC4 WEB API.
        The JQGrid as is returns a date as “dd/mm/yyyy”
        I don’t know what is easiest :
        – fitting moment.js into jqgrid.js at the client or
        – adapting the server-side model binding.
        As for the second approach : using your method with
        var binder = new DateTimeModelBinder(“nl-BE”);
        has no effect. This culture is also defined in Web.config.

        Thanks

      • You’re right – my comment above is incorrect. GetCustomDateFormat should return the *date format*, not the culture. You can get the culture using new CultureInfo(“en-US”).DateTimeFormat.ShortDatePattern

        I think that the best approach in this case is to use a consistent date format (such as yyyy-MM-dd) for all transfer. Otherwise you will have problems if the culture on either your server or one of your clients is ever something unexpected.

  2. Mark Van Mierlo says:

    Thanks again.
    I tried the following in Global.asax.cs :
    var cultureFormat = new CultureInfo(“nl-BE”).DateTimeFormat.ShortDatePattern;
    var binder = new DateTimeModelBinder(cultureFormat);
    No change.

    I’m exploring the possibilities of MVC4 and JQGrid for a local Intranet-application, So diverging timezones are not an issue. Getting valid dates back from the client is.
    Sending dates to the client is no problem, I tried a lot of different formats and now use ISO8601Long.
    However JQGrid seems to send back (POST or OUT) only in one format : “mm/dd/yyyy”.
    I didn’t find out yet which module does the parsing: jqgrid, jquery or the browser.
    I realize this is a JQGrid-issue but I thought to tackle it with your binding-solution. ;-)

  3. Mark Van Mierlo says:

    The method signatures in my Web API-controller are :
    public HttpResponseMessage Put(int id, Product item)
    and
    public HttpResponseMessage Post(Product item)
    There is only one DateTime-field in the model.

    I placed a breakpoint in the BindModel-method and it indeed is not hit.
    Have I missed something ?
    I just created the DateTimeModelBinder-class and initated it in Global.asax.

    • Ah, In suspect the problem is in fact that WebAPI uses different base classes to MVC, and the code in this post is for MVC.

      You should still be able to create a custom binder for WebAPI – you’ll just need to implement System.Web.Http.IModelBinder instead of System.Web.Mvc.IModelBinder. I’ve not looked at it but I assume it’s a fairly similar interface

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s