In ASP.NET MVC, our system is built such that the interactions with the user are handled through Actions on our Controllers. We select our actions based on the route the user is using, which is a fancy way of saying that we base it on a pattern found in the URL they’re using. If we were on a page editing an object and we clicked the save button we would be sending the data to a URL somewhat like this one.
Notice that in our route that we have specified the name of the object that we’re trying to save. There is a default Model Binder for this in MVC that will take the form data that we’re sending and bind it to a CLR objects for us to use in our action. The standard Edit action on a controller looks like this.
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
try
{
// TODO: Add update logic here
return RedirectToAction("Index");
}
catch
{
return View();
}
}
If we were to flesh some of this out the way it’s set up here, we would have code that looked a bit like this.
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
try
{
Profile profile = _profileRepository.GetProfileById(id);
profile.FavoriteColor = collection["favorite_color"];
profile.FavoriteBoardGame = collection["FavoriteBoardGame"];
_profileRepository.Add(profile);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
What is bad about this is that we are accessing the FormCollection object which is messy and brittle. Once we start testing this code it means that we are going to be repeating code similar to this elsewhere. In our tests we will need to create objects using these magic strings. What this means is that we are now making our code brittle. If we change the string that is required for this we will have to go through our code correcting them. We will also have to find them in our tests or our tests will fail. This is bad. What we should do instead is have these only appear on one place, our model binder. Then all the code we test is using CLR objects that get compile-time checking. To create our Custom Model Binder this is all we need to do is write some code like this.
public class ProfileModelBinder : IModelBinder
{
ProfileRepository _profileRepository = new ProfileRepository();
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
int id = (int)controllerContext.RouteData.Values["Id"];
Profile profile = _profileRepository.GetProfileById(id);
profile.FavoriteColor = bindingContext
.ValueProvider
.GetValue("favorite_color")
.ToString();
profile.FavoriteBoardGame = bindingContext
.ValueProvider
.GetValue("FavoriteBoardGame")
.ToString();
return profile;
}
}
Notice that we are using the form collection here, but it is limited to this one location. When we test we will just have to pass in the Profile object to our action, which means that we don’t have to worry about these magic strings as much, and we’re also not getting into the situation where our code becomes so brittle that our tests inhibit change. The last thing we need to do is tell MVC that when it is supposed to create a Profile object that it is supposed to use this model binder. To do this, we just need to Add our binder to the collection of binders in the Application_Start method of our GLobal.ascx.cs file. It’s done like this. We say that this binder is for objects of type Profile and give it a binder to use.
ModelBinders.Binders.Add(typeof (Profile), new ProfileModelBinder());
Now we have a model binder that should let us keep the messy code out of our controllers. Now our controller action looks like this.
[HttpPost]
public ActionResult Edit(Profile profile)
{
try
{
_profileRepository.Add(profile);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
That looks a lot cleaner to me, and if there were other things I needed to do during that action, I could do them without all of the ugly binding logic.
Comments