In the last section of my MVC Templates post, I discussed how to use the Html.EditorFor() helper with complex types. Whilst this showed how you can use Html.EditorFor() and templates to produce a user interface for complex types, it didn’t show you how to bind the result when the user has posted to a controllers action. In this post I will extend the employee example, so that it displays the offices the the user selected.
Contents
Download the code for this example
Default Model Binders
The concept of Model Binders was introduced in MVC Preview 5, before Model Binders in a controllers action we would either get values directly from the Request Context, for example Request[“Foo”] or bind directly to the Action’s parameters, for example public ActionResult Foo(string Bar). This is OK but can get untidy if you have more than a couple of Model properties to bind. Based on the request context the DefaultModelBinder will bind our Model’s properties for us, hence when creating templates it’s important to understand how to create the correct markup for your input fields using TemplateInfo. The supported types for the DefaultModelBinder are Primitive types, such as String etc, Model classes, and generic collections List<T> etc.
Before I discuss Model Binders, lets examine what happens if we select Offices using the DefaultModelBinder. Debug\Employee\Index provide a First Name, Last Name, and Office then click on the Create button, you will see the following validation message:
The reason for this is that the DefaultBinder doesn’t know how to bind our List<Office> from a Select input field.
I Thought the DefaultBinder Supported Collections?
Yes, but only in a specific way, I’ll discuss a Alternative Approach later in the post. For this example our requirements are to bind using a Select input field, in my opinion creating a simple Custom Model Binder is simplest way to do this.
Custom Model Binders
Below are the changes required to add a Custom Model Binder to my previous complex types example.
First of all a little house keeping, we need to remove the Office List from the Index action into it’s own method and class. In a real situation you would get the data from a infrastructure service such as a database.
Paste this standalone class into EmployeeModel.cs
//TODO This should be replaced by a infrastructure service such as a database public static class ModelData { public static List OfficeData = new List(new Office[]{ new Office{ OfficeId = new Guid("ee70f79e-3840-4e8f-98eb-aa8ee12e254c"), Address = "One Microsoft Way, Redmond", Country = "United States", PostalCode = "WA 98052" }, new Office{ OfficeId = new Guid("0780b91a-7000-4860-a89c-ed6a1ea6c071"), Address = "Microsoft Campus, Thames Valley Park, Reading, Berkshire", Country = "United Kingdom", PostalCode = "RG6 1WG" }, new Office{ OfficeId = new Guid("db7a5076-c7c7-42f3-a6c6-1ead82af310a"), Address = "Centro Direzionale San Felice, Palazzo A. Via Rivoltana, 13", Country = "Italy", PostalCode = "1320090 Segrate (MI)" } } ); }
Next we need to create our own Model Binder, create a folder called Binders, add a new class and name it EmployeeBinder.cs.
Then paste the code below into your new class:
using System; using System.Collections.Generic; using System.Linq; using System.Web.Mvc; using MvcTempDemo1.Models; namespace MvcTempDemo1.Binders { public class EmployeeBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var model = base.BindModel(controllerContext, bindingContext) as EmployeeModel; //Bind most of the model using the built-in binder, this will bind all primitives for us const string magicString = "Offices"; //TODO Use of magic strings var offices = bindingContext.ValueProvider.GetValue(magicString); //Get the posted value for offices if (offices != null && !string.IsNullOrEmpty(offices.AttemptedValue)) { //Check we have a value for offices before we proceed bindingContext.ModelState.Remove(magicString); //Remove binding conversion errors for offices, as we are going to deal with binding offices ourselves try { //Create a list of offices based on the comma delimited string of posted OfficeId's model.Offices = new List( offices.AttemptedValue.Split(",".ToCharArray()) .Select(id => new Office() { OfficeId = new Guid(id) }) .ToList() ); } catch (FormatException ex) { //Catch if the posted offices are not posted as a comma delimited string of Guids bindingContext.ModelState.AddModelError(magicString, ex); //Add an error to the model state, used for ModelState.IsValid and Html error helpers } catch (Exception ex) { //Unexpected exception throw ex; } } return model; } } }
How Does This Work?
We only want to do our own model binding for the Offices model property which is the type of List<Office>, for everything else primitive string’s, DateTime’s etc we want to use default model binding.
Therefore we inherit the DefaultModelBinder, as it would be silly to reinvent the wheel when the default does most of what we want.
Next we call base.BindModel this does a all the Default Binding for us and returns an object representing our model, we then convert the bound object to our EmployeeModel using the as keyword.
We now have a bound EmployeeModel, now we need to add some code to do our custom binding for the Offices property.
First we need to get the posted value for Offices, the preferred way is via ValueProvider.GetValue, rather then using the Request Context directly for example Request[“Foo”].
Next we check that a value was posted for Offices.
Then we remove any ModelState errors for Offices, as we will be binding Offices ourselves (there will be conversion errors from the call to base.BindModel).
We know that the Offices value will be posted as an encoded set of Office id’s with multiple values delimited by a commas. All we do is split the string value will got via ValueProvider.GetValue and create a new List<Office> using just the posted Office id’s, we then set the model.Offices property to this List.
We catch any FormatExceptions which occur, for example if an id was posted that was not the type of GUID, the exception is added to ModelState for use in ModelState.IsValid and Html helpers. Any unexpected exceptions are thrown.
Finally we return the Model, hopefully with the Offices model property populated with the ids of the Offices the user selected.
How to Wire-Up a Custom Model Binder?
For a particular model the easiest way to tell the MVC framework to use your custom model binder is by decorating the model with the ModelBinder attribute. The ModelBinder attribute takes one parameter the type of your model binder. The ModelBinder attribute adds your ModelBinder to the ModelBinderDictionary. You can also add your to your model to the ModelBinderDictionary via the Application_Start() event in the Global.asax file. I prefer the former method as it is more obvious what’s going on and feels more like convention over configuration.
To wire up our custom model binder, lets add the ModelBinder attribute to our EmployeeModel. Change the EmployeeModel class in Models\EmployeeModel.cs:
[ModelBinder(typeof(EmployeeBinder))] public class EmployeeModel { [Required] [DisplayName("First Name")] public string FirstName { get; set; } [Required] [DisplayName("Last Name")] public string LastName { get; set; } [DisplayName("Date of Birth")] public DateTime? DateOfBirth { get; set; } [DataType("Offices")] public List Offices { get; set; } }
Change the Details Action
We need to make some simple changes to the EmployeeController’s Details action. In Controllers\EmployeeController.cs change the Details action as follows:
[HttpPost] public ActionResult Details(EmployeeModel model) { if (ModelState.IsValid) { //TODO This should be replaced by a infrastructure service such as a database if (model.Offices != null) { var offices = new List(); //Get offices by OfficeId that where selected foreach (var office in model.Offices) { offices.Add( ModelData .OfficeData .Single(o => o.OfficeId == office.OfficeId) ); } model.Offices = offices; } // return View(model); } else { return RedirectToAction("Index", model); } }
This change just selects the Office by it’s id using the model which was bound using our custom model binder. In a real scenario this would be a call to an infrastructure service such as a database.
Create a Display Template
Add a shared display template for Offices, if you don’t know how to do this please read my previous post. Then add the following code to \Views\Shared\DisplayTemplates\Offices.ascx:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<List<MvcTempDemo1.Models.Office>>" %><%=Html.LabelFor(model => model)%><%=Html.Encode(string.Join("; ", Model.Select(model => model.Country).ToArray()))%>
Finally in the Details view add the DisplayFor helper for Offices. Paste the following code into Views\Employee\Details.aspx:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MvcTempDemo1.Models.EmployeeModel>" %>
Details
Details
<%= Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) %> |
<%= Html.ActionLink("Back to List", "Index") %>
Debug\Employee\Index provide a First Name, Last Name and Office then click on the Create button.
As you can see the Office selection is now populated with the rest of the information.
Custom Model Binders Best Practice
Just a quick word on best practice for Custom Model Binders, the only function of a Model Binder is to bind the HTTP request data to the Model and add any validation errors to the ModelState. It should be as lightweight as possible, it shouldn’t call any infrastructure services such as a database or web service, it should deal with binding and only binding. I’ve seen some examples of people calling out to databases via a custom model binder, this approach will: add dependencies, make it harder to unit test and breaks separation of concerns. Imagine if the DefaultModelBinder was dependent on a database 🙂
Improvements
Apart from the obvious missing data service, the use of a magic string in the Custom Model Binder makes the code a little bit fragile, a simple change would be to select the property by type. This would mean you could re-use the Model Binder for any Model that has List<Office> type regardless of what the property name is. In summary using magic strings means you are binding using the property name rather than the type, therefore losing the opportunity of code reuse by type and affecting the way you would unit test the Model Binder.
Alternative Approach
As mentioned binding to Lists and Collections is supported, but it’s a little bit specific, namely you need to use hidden fields/textboxes, Phil Haack has written an excellent post on this subject. Whilst you could get the DefaultBinder to work with a select field, a custom Model Binder in my opinion is a more elegant approach.
Conclusion
One of the reasons the MVC framework is brilliant is – if it doesn’t quite do what you require just extend the default behavior. I hope this post demonstrated how easy it is to create your own Model Binder and that in the MVC world you rarely need to compromise on your Models or User interfaces because if it doesn’t work for you just extend the functionality.
For more tips on Model Binders see Scott Allen’s excellent post.
good information, you write it very clean. I am very lucky to get this tips from you.
Beautiful code and explanation. Thanks for the hard work! 😉
[…] nasty hacks. In this post we are going to continue our Employee example from my MVC Templates and MVC Model Binders posts, we will change the code to make use of the new […]
Could you post an example of the improvement you proposed by not using the magic string? I’m curious how that would work. Finding decent articles about things like model binders is not easy. Your posts have been extremely helpful. Thanks
Great article. In the custom model binder, is there a way to re-validate the model after you update it with the List of Offices? Say your Offices model had DataAnnotations on it. Is there a way to validate the model and have it update the ModelState with any errors?
Thank you. Finally a decent article on the basics model binding (well, I’d not come across any at least!), helped me out a lot 🙂
Thanks for this – very concise. I’m stuck at an architectural challenge, that you touch on in your Best Practice section. In real life, the Offices are out in a database somewhere, so how do you go fetch the appropriate record in a ModelBinder.
I agree that the ModelBinder should not be calling directly to the database, but the only other option I can see is to add all the Offices into the ViewBag (or similar) and iterate through them via the controllerContext.
But that seems quite expensive, especially if you had 43,000 offices…
You shouldn’t connect to the database via a model binder instead get the data from the database in the controllers action. The model binder is for binding the http request to your model, from the controllers action you then use the model values to query your database. If your worried about performance consider system.web.caching or output caching, if you have the choice go for output caching
I echo Andrew Knox here in saying finally, a decent post on how to easily extend the default model binder.
It’s worth noting that you can also use the ModelBinder attribute on an controller’s action parameter, which is useful if the custom ModelBinder is required only for certain actions.