MVC ModelBinder and Localization
One of the great things about the way in which the Microsoft ASP.NET MVC Framework is being developed is the fact that the team are publishing the source code as they go along. This makes it very simple to dig into the framework when something slightly surprising is happening and figure out exactly how it is supposed to work. A good example of this cropped up for us yesterday when looking at a form posting scenario using DateTimes. We were dealing with an Action Method that looked something like this:
1: public ActionResult DoSomething(DateTime theDate)
3: return View();
And using the automatic model binding infrastructure to set the method parameter from a form. Naturally, being based in England, we were not totally shocked when we ran into localization issues almost immediately with this. It appeared at first glance that the date was being parsed assuming en-US format (mm/dd/yyyy), whereas for the application we were writing, which will be used internally be people who will expect to use en-GB (dd/mm/yyyy), we wanted it to parse in a different format. This prompted some investigation to discover what the framework was using to make its decision about which culture to use. The result turned out to be somewhat more clever than we had first thought, and nicely illustrates the way in which MVC Framework applications should be structured.
The initially surprising piece of information that it transpires that it actually matters whether you have set the HTTP method to be a GET or a POST. To understand why this is the case, here is a snippet from within the MVC source code. I have removed several lines from the code to make it easier to see what is going on. If you want to see the original, it is within GetValue(string name) of System.Web.Mvc.DefaultValueProvider.
1: CultureInfo culture = CultureInfo.InvariantCulture;
3: if (request.QueryString != null)
5: rawValue = request.QueryString.GetValues(name);
7: if (rawValue == null && request.Form != null)
9: culture = CultureInfo.CurrentCulture;
10: rawValue = request.Form.GetValues(name);
In other words, when looking for the value to parse, the framework looks in a specific order namely:
- RouteData (not shown above)
- URI query string
- Request form
Only the last of these will be culture aware however. There is a very good reason for this, from a localization perspective. Imagine that I have written a web application showing airline flight information that I publish online. I look up flights on a certain date by clicking on a link for that day (perhaps something like http://www.melsflighttimes.com/Flights/2008-11-21), and then want to email that link to my colleague in the US. The only way that we could guarantee that we will both be looking at the same page of data is if the InvariantCulture is used. By contrast, if I'm using a form to book my flight, everything is happening in a tight cycle. The data can respect the CurrentCulture when it is written to the form, and so needs to respect it when coming back from the form.
But this brings us back to a consideration of the HTTP method that has been used. Remember that if we set the form to be HTTP GET, when the form is submitted the values of the fields in the form will be turned into a query string. So they will be parsed with the InvariantCulture rather than the CurrentCulture, as they would have been if it were an HTTP POST. We can therefore toggle the behaviour of the form (from a localization standpoint) by changing the HTTP method.
Now to me this seemed slightly surprising at first, but I can certainly see that from the framework authors' perspective, it's the correct way of doing things. It still does leave one loose end, from a technical perspective. Suppose now that out airline flight information application allows the user to type in a date for which they wish to view flight information rather than clicking on a (computer generated) hyperlink. This is not an unreasonable behaviour, and is probably far more realistic of the way that the application would in fact be written. I still want the form to submit via HTTP GET rather than POST, as I want a URL to be generated that my user could email to his colleague. Effectively therefore, I have mandated that the user needs to enter the date using the InvariantCulture, which is hardly very localization friendly! If the application is only going to be used by people in (for example) en-GB (as might be the case for a company internal application), it is very possible that the date format will be known, and specified.
Help is at hand though! Luckily, the way that the MVC Framework has been written makes it very easy for us to drop in other components to ensure that specific parts of the application such as this run the way that we expect them to. In this instance, what we want to do is register a new ModelBinder that will handle this DateTime to parse it using a specified culture. Writing a new ModelBinder is actually staggeringly simple. All we need to do is implement IModelBinder, and then register the new ModelBinder in Global.asax.cs. A very simple example of a custom ModelBinder to force the binding of DateTimes to always use en-GB is given below.
1: public class MyBinder : IModelBinder
4: #region IModelBinder Members
6: public ModelBinderResult BindModel(ModelBindingContext bindingContext)
8: string theDate = bindingContext.HttpContext.Request.QueryString["theDate"];
9: DateTime dt = new DateTime();
10: bool success = DateTime.TryParse(theDate, CultureInfo.GetCultureInfo("en-GB"), DateTimeStyles.None, out dt);
11: if (success)
13: return new ModelBinderResult(dt);
17: // Return an appropriate default
We then just need to tell MVC to use the ModelBinder. All this takes is a line in Global.asax saying:
1: ModelBinders.Binders.Add(typeof(DateTime), new MyModelBinder());
Of course the custom ModelBinder could be modified to do anything else that you want to, or indeed descend from DefaultModelBinder, and use other properties of the binding to help it determine whether it should use the new behaviour, or leave it up to the original DefaultModelBinder. The above example isn't particularly realistic in that we have mandated that the value of a DateTime will be coming in on a query string parameter of 'theDate'. It would normally be the case that we would use other properties of the ModelBindingContext to be more accurate about the situations we need to override. The ModelBinder could also be specified explicitly using an attribute on the method parameter.
On a side note, I thought it would be worth linking to an article featuring Red Gate's biggest fan!