Globalization: Model binding DateTimes with ASP.Net MVC

Hackered
Sunday, February 8, 2015
by Sean McAlinden

Dates and calendars can be a little tricky when working on Global websites, especially as ASP.Net MVC will not model bind DateTime viewmodel properties from different UI cultures without a little help.

We can solve this issue using simple custom model binders.

Firstly we should try and understand why this is an issue.

Why don't ASP.Net MVC model binders handle globalized dates for us out of the box?

Well there are a few good reasons for this, but a key reason for me is idempotent URL's.

If a DateTime property helped form a URL, then the resources URL should not change depending on your browser settings or the server locale settings.

Also, it should be a choice of the application developer whether to utilise locale information for site resource selection.

So, how is it done?

Out of the box, ASP.Net MVC also does not set the Thread.CurrentThread.CurrentUICulture to the clients Accept-Language header preferences, this is something you will need to do yourself, for the purposes of testing out the model binders, you can simple put the following into your Global.asax and manually change the country ISO code to test:

protected void Application_BeginRequest()
{
    Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
}

If I get chance I'll do a little post on ways to set the culture depending on preferences, however it's not the goal of this post.

As I use DateTime and nullable DateTime? properties, I am going to create a custom model binder for each type.

Custom DateTime model binder

public class DateTimeModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        DateTime dateTime;

        var isDate = DateTime.TryParse(value.AttemptedValue, Thread.CurrentThread.CurrentUICulture, DateTimeStyles.None, out dateTime);

        if (!isDate)
        {
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, ModelValidationResources.InvalidDateTime);
            return DateTime.UtcNow;
        }

        return dateTime;
    }
}

As you can see I grab the value of the property, try and parse it using the Thread.CurrentThread.CurrentUICulture, if it is indeed a date I return it, otherwise I return a valid date with an error message from my resources file.

To utilize this model binder, I added the following to the Application_Start() method in the Global.asax:

System.Web.Mvc.ModelBinders.Binders.Add(typeof(DateTime), new DateTimeModelBinder());

Great, we can now model bind DateTime properties matching our CurrentUICulture.

Custom nullable DateTime? model binder

public class NullableDateTimeModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        if (string.IsNullOrWhiteSpace(value.AttemptedValue))
        {
            return null;
        }

        DateTime dateTime;

        var isDate = DateTime.TryParse(value.AttemptedValue, Thread.CurrentThread.CurrentUICulture, DateTimeStyles.None, out dateTime);

        if (!isDate)
        {
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, ModelValidationResources.InvalidDateTime);
            return DateTime.UtcNow;
        }

        return dateTime;
    }
}

Similar to the DateTime custom model binder, I grab the property value, this time I return null if the value is null or whitespace.

I then try and parse the value using the Thread.CurrentThread.CurrentUICulture, if it is a nulable DateTime? I return it, otherwise I return a valid date with an error message from my resources.

To utilize this nullable DateTime? model binder, I added the following to the Application_Start() method in the Global.asax:

System.Web.Mvc.ModelBinders.Binders.Add(typeof(DateTime?), new NullableDateTimeModelBinder());

Great, we can now model bind nullable DateTime? properties matching our CurrentUICulture.

That's it...

There you have it, a relatively simple approach to handling DateTime globalization with ASP.Net MVC and custom model binders.