Model binding i .NET MVC

Posted by Kenny Eliasson | Posted in , | Posted on 15:55

(From now on I will try to write my posts in english)
I see alot of variations in how people do model-binding in .NET MVC.
Some guys use the old-school way using Request.Form directly in their Controller actions.

public ActionResult Create()
{
Recipe recipe = new Recipe();
recipe.Name = Request.Form["Name"];
return View();
}

A better way is to pass a FormCollection to the action.
public ActionResult Create(FormCollection values)
{
Recipe recipe = new Recipe();
recipe.Name = values["Name"];

// ...

return View();
}

The best way is of course to accept your entity directly.
public ActionResult Create(Recipe recipe)
{
_repository.Save(recipe);
return View();
}

This works great when dealing with simple objects.
But what happens when you have a rich domain model and your entities references other entities and so on..

Take this simple class for examle
public class Comment
{
public int Id { get; set; }
public string Message { get; set; }
public Post Post { get; set; }
}

If Post is a reference to entity that's already saved in the database. How do you populate it nicely?
Either you can let the ModelBinder bind what it can, and in your action method do
public ActionResult Create(Comment comment)
{
comment.Post = _repository.Load(Request.Post["PostId"]);
_repository.Save(comment);
return View();
}

This works, but I dont really like it :P

I return to a solution further down.

Another thing I often find irritating is the need to inject a repository into a controller only to read up a entity.
Instead of having to do like this
public class PostController : Controller
{
private readonly IRepository _repository;
public PostController(IRepository repository)
{
_repository = repository;
}

public ViewResult Details(int id)
{
return View(_repository.Get(id));
}
}

I would instead like it to be like this.
public class PostController : Controller
{
public ViewResult Details(Post post)
{
return View(post);
}
}

Now this was a very simple case, but even then i think it has several advantages, one being that it's easier to unit-test this method and another being that we can hide alot of repetetive code.

How do I accomplish both binding to Details/Show-actions as well as binding more complex objects?

Answer: I wrote my own modelbinder ;)
public class GenericBinderResolver : DefaultModelBinder
{
private readonly ICanResolveDependencies _resolver;
private static readonly Type BinderType = typeof(IModelBinder<>);

private class ModelBindResult
{
public bool Success;
public object BoundObject;
}

///
/// Creates a new GenericBinderResolver. This should be used as the DefaultModelBinder
///

/// For easy testability of this class we created a new interface thats extracts out the "GetInstance" method from StructureMap.
public GenericBinderResolver(ICanResolveDependencies resolver)
{
_resolver = resolver;
}

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//First look at the routes to see if we have any parameters named "Id", if we do load the entity from the database
var result = TryBindById(bindingContext, controllerContext);
if(result.Success) {
return result.BoundObject;
}

//If there wasn't a Id in the route check if we have custom modelbinder for the type.
result = TryBindWithCustomModelBinder(bindingContext, controllerContext);
if (result.Success) {
return result.BoundObject;
}

//If nothing works, do it the usual way.
return base.BindModel(controllerContext, bindingContext);
}

private ModelBindResult TryBindById(ModelBindingContext bindingContext, ControllerContext controllerContext)
{
var result = new ModelBindResult();

int entityId = 0;
//Is the type we're binding to assignable to our base domain object.
if (IsAssignableToDomainObject(bindingContext.ModelType))
{
if (EntityExistsInRoute(controllerContext.RouteData.Values, bindingContext.ModelName, out entityId))
{
result.Success = true;
result.BoundObject = _resolver.TryGetInstance().Get(bindingContext.ModelType, entityId);
}
}

return result;
}

private ModelBindResult TryBindWithCustomModelBinder(ModelBindingContext bindingContext, ControllerContext controllerContext)
{
var result = new ModelBindResult();
Type genericBinderType = BinderType.MakeGenericType(bindingContext.ModelType);
IModelBinder binder = _resolver.TryGetInstance(genericBinderType) as IModelBinder;
if (binder != null) {
result.Success = true;
result.BoundObject = binder.BindModel(controllerContext, bindingContext);
}
return result;
}

private bool EntityExistsInRoute(RouteValueDictionary routeValueDictionary, string modelName, out int entityId)
{
entityId = TryParseRouteData(routeValueDictionary, "id");
if (entityId > 0)
return true;

entityId = TryParseRouteData(routeValueDictionary, string.Concat(modelName, "id"));
if (entityId > 0)
return true;

return false;
}

private int TryParseRouteData(RouteValueDictionary routeValueDictionary, string key)
{
if (routeValueDictionary.ContainsKey(key))
{
string id = routeValueDictionary[key].ToString();
if (!string.IsNullOrEmpty(id))
{
return int.Parse(id);
}
}

return 0;
}

private bool IsAssignableToDomainObject(Type modelType)
{
return typeof(DomainObject).IsAssignableFrom(modelType);
}
}

Alot of code but the important things is that I inject a instance of ICanResolveDependencies which is a implementation of your favorite IoC-container. In my case StructureMap.

I then look at the route data for a key named "id" if the ModelBindingContext.ModelType is a DomainObject which happens to be my base-class for all entites.
So routes like "Post/Detail/5" would have a key named "id". I take the key and use it with to get the entity from the database. And whops i solved a problem.

Also important to note is that if the ModelType isn't is a DomainObject nothing happens and I will let the default model-binder do its work.

The next thing I do is to check if I have custom model-binder class for the ModelType. All my custom model-binders inherit from the same base-class which checks the Request.Form and Querystring dictionaries for a key named "id". If it finds a key named "id" and its greater than 0 it will call the abstract method "BindExisting(int originalId)" else it calls the abstract method "BindNew()".

A implementation of a custom model-binder can look like this
public class CommentBinder : ModelBinder
{
private readonly INHibernateRepository _repository;
private readonly IUserContext _context;

public CommentBinder(INHibernateRepository repository, IUserContext context)
{
_repository = repository;
_context = context;
}

protected override Comment BindNew()
{
//Don't do more work then necessary, I have method on the base-class which does the default model-binding.
var comment = base.DoDefaultModelBinding();

//Since I use NHibernate, just get a proxy to the Post and don't hit the database.
//The Get() method is within the base-class and just returns Request-data.
comment.Post = _repository.Load(Get("PostId"));
comment.User = _context.User;

return comment;
}

protected override Comment BindExisting(int entityId)
{
//load the entity and change the values
_repository.Get(entityId);

var comment = base.DoDefaultModelBinding();

comment.Post = _repository.Load(Get("PostId"));
comment.User = _context.User;

return comment;
}
}

(I left out the code for the base class, if you want too see it email me kennyeliasson at gmail dot com)

All I need to care about is how to handle existing and new objects.
This solution have worked out really nice for me WHEN i have more complex objects. Of course I use the default model-binding as often as i can, but sometimes it just isn't enough.

Comments (1)

  1. Smart decision to write in english! :)

Skicka en kommentar