The good part: you just saved a lot of code, which is good for efficiency and for supporting/debugging.

The bad part: what happens when we edit/update an object and the form does not include all the fields? We just overwrote the value to the default .NET value and saved to the db.

For example, if the model record had a property called [phone_number], and this MVC form did not have it. Maybe the form had to hide some values from update, or else the data model changed and added a field. In an Edit/update, the steps would be:

creates the object from the class,

copy the values from the form

save/update to the db

… we never actually grab the current values of [phone_number], and we just set it to the .NET default value for the string type. Lost some real data. Not good.

ActionResult method and Model Binder steps

What’s actually happening:

framework looks at the parameter type and executes the registered IModelBinder for it. If there is none, it uses DefaultModelBinder

create a new instance of the model – default values , i.e. default(MyModel)

read the form POST collection from HttpRequestBase

copy all the matching fields from the Request collection to the model properties

run it thru the MVC Validator, if any

return it to the controller ActionResult method for further action

Writing code in the Action method to fix the problem

My first step to deal with the issue was to fall back to the FormCollection model binder and hand-code the fix. It looks something like this:

[HttpPost]
publicActionResult Edit(int id, MyCompany.POCO.MyModel model, FormCollection collection)
{
// updateif (!ModelState.IsValid)
{
return View("Edit", model);
}
var poco = modelRepository.GetByID(id);
// map form collection to POCO// * IMPORTANT - we only want to update entity properties which have been // passed in on the Form POST. // Otherwise, we could be setting fields = default when they have real data in db.foreach (string key in collection)
{
// key = "Id", "Name", etc.// use reflection to set the POCO property from the FormCollection
System.Reflection.PropertyInfo propertyInfo = poco.GetType().GetProperty(key);
if (propertyInfo != null)
{
// poco has the form field as a property// convert from string to actual type
propertyInfo.SetValue(poco, Convert.ChangeType(collection[key], propertyInfo.PropertyType), null);
// InvalidCastException if failed.
}
}
modelRepository.Save(poco);
return RedirectToAction("Index");
}

In this example, modelRepository could be using NHibernate, EF, or stored procs under the hood, but it could be any data source. We loop thru each form post key and try to find a matching property on the model (using reflection). If it matches, convert the string value from the form collection and set it as the value for that propery (also using reflection).

This works and is good, until you realize you have to insert it into every Action method. We could also go traditional, and just stick it in a function call. But we want to leverage the MVC convention-over-configuration philosophy. So now we’re going to try wrapping it in a custom model binder class.

Creating a Custom Model Binder to fix the problem

To avoid the “unspecified field” problem, we want a model binder to actually do the following on Edit:

Get() the model from the repository by id to create a new instance of the model

Update the fields of the persisted model which match from the FormCollection

run it thru the MVC Validator, if any

return it to the controller ActionResult method for further action (like Save() )

I am going to define a generic class which is good for any of my POCO types, and inherit from DefaultModelBinder:

As you can see, with CreateModel(), if it is an Edit call, we retrieve the model object by the id specified in the URL. This is already parsed out in the RouteData collection. If it is not an Edit, we just call the base class CreateModel(). For example, a Create() call may also use the same ModelBinder.

Now, in the BindModel() method, this is where we move our logic to iterate thru the Form key/value pairs and update the POCO. But in this version, we only update fields in the form, and leave other properties alone:

publicoverrideobject BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object model = this.CreateModel(controllerContext, bindingContext);
// map form collection to POCO// * IMPORTANT - we only want to update entity properties which have been // passed in on the Form POST. // Otherwise, we could be setting fields = default when they have real data in db.foreach (string key in controllerContext.HttpContext.Request.Form.Keys )
{
// key = "Pub_id", "Name", etc.// use reflection to set the POCO property from the FormCollection// http://stackoverflow.com/questions/531025/dynamically-getting-setting-a-property-of-an-object-in-c-2005// poco.GetType().GetProperty(key).SetValue(poco, collection[key], null);
System.Reflection.PropertyInfo propertyInfo = model.GetType().GetProperty(key);
if (propertyInfo != null)
{
// poco has the form field as a property// convert from string to actual type// http://stackoverflow.com/questions/1089123/c-setting-a-property-by-reflection-with-a-string-value
propertyInfo.SetValue(model, Convert.ChangeType(controllerContext.HttpContext.Request.Form[key], propertyInfo.PropertyType), null);
// InvalidCastException if failed.
}
}
return model;
}

Great. Now that we have our ModelBinder, we have to tell our MvcApplication to use it. We add it the following line to Application_Start():

In english, we are saying: Add to the ModelBinder collection… when you have to Model Bind a MyCompany.POCO.MyModel, use the PocoModelBinder<> (and pass it an IPocoRepository so it can access the data store).

Now we’re able to run our app, and can do safe, smart updates the “MVC-way”, keeping our methods clean.

I’ve use the Castle Windor IoC container and any NHibernate-backed Repository in this case, but the same technique can be used in any ASP.NET MVC app using any data access backend, and with or without any IoC container.