01 March 2010 - 11:50 PM / by Dominic Pettifer. 0 Comments for Dependency Injection in ASP.NET MVC 2 – Part 2: ModelBinders/ViewModels.
Technical Article - In part 2 of a series on Dependency Injection in ASP.NET MVC, we look at injecting dependencies into your ViewModels. This technique comes in useful for when you want to render a dropdown list of items from a database, and don’t want your controller populating the items.
Part 1 covered the ins and outs of Dependency Injection and IOC Containers in general, so check out that article for an overview. In part 2 we look to use the Castle Windsor IOC Container to inject dependencies into our ViewModels by using a custom ModelBinder. But why?
It's common to use custom ViewModels for each view in your ASP.NET MVC application, rather than send in a domain object. You create a ViewModel with properties tailored to the needs of the view. This has maintainability, type safety, and security benefits, especially when using a ViewModel to represent a web-form for POSTing data back to the server. Consider the following ProductForm ViewModel:
public class ProductForm
{
[Required]
public string ProductName { get; set; }
public string Description { get; set; }
[Required]
public string Price { get; set; }
[Required]
public string Quantity { get; set; }
[Required]
public int? ParentCategoryId { get; set; }
public IList<Category> Categories { get; set; }
}In our strongly typed aspx view we use the 'Categories' property to fill in a drop down list of Categories for the user to select a parent Category for the Product (which is then assigned to the ParentCategoryId property on post-back). The Categories collection will need to be filled in though, via our Controller:
public class ProductsController : Controller
{
protected ICategoryRepository CategoryRepository = null;
public ProductsController(ICategoryRepository categoryRepository)
{
this.CategoryRepository = categoryRepository;
}
[HttpGet]
public ActionResult Add()
{
IList<Category> categories = CategoryRepository.ListAllCategories();
ProductForm form = new ProductForm();
form.Categories = new List<Category>();
foreach (Category category in categories)
{
form.Categories.Add(category);
}
return View(form);
}
[HttpPost]
public ActionResult Add(ProductForm form)
{
if (!ModelState.IsValid)
{
IList<Category> categories = CategoryRepository.ListAllCategories();
form.Categories = new List<Category>();
foreach (Category category in categories)
{
form.Categories.Add(category);
}
return View(form);
}
Product product = new Product();
product.Name = form.ProductName;
product.Description = form.Description;
// ...fill out other properties and persist to database (snip)... //
return RedirectToAction("Edit");
}
}All well and good except we're always having to manually populate the ViewModel's Categories collection, including when model validation fails after POSTing and we have to re-display the View. This can get tedious having to type this each time we want to use the ProductForm, and violates the DRY (Don't Repeat Yourself) principle. We could encapsulate the Category population code in a method, or we could get the ViewModel itself to take care of it. Simply pass in the ICategoryRepository into the ProductForm constructor:
ProductForm form = new ProductForm(CategoryRepository);
...and change the ProductForm class constructor to the following:
public class ProductForm
{
public ProductForm(ICategoryRepository categoryRepository)
{
this.Categories = new List<Category>();
foreach (Category category in categoryRepository.ListAllCategories())
{
this.Categories.Add(category);
}
}
// ...other properties (snip)... //
public IList<Category> Categories { get; set; }
}...and the Controller code becomes a lot simpler:
public class ProductsController : Controller
{
protected ICategoryRepository CategoryRepository = null;
public ProductsController(ICategoryRepository categoryRepository)
{
this.CategoryRepository = categoryRepository;
}
[HttpGet]
public ActionResult Add()
{
return View(new ProductForm(CategoryRepository));
}
[HttpPost]
public ActionResult Add(ProductForm form)
{
if (!ModelState.IsValid)
{
return View(form);
}
Product product = new Product();
product.Name = form.ProductName;
product.Description = form.Description;
// ...fill out other properties and persist to database (snip)... //
return RedirectToAction("Edit");
}
}Except now we have a problem, the above code will throw an error as soon as we try to submit (POST) a Product.
ASP.NET MVC's DefaultModelBinder is responsible for magically taking your form's input parameters and setting them against the properties of your ProductForm object passed into the Add() method above. However, the DefaultModelBinder doesn't know about our IOC Container, and it expects there to be a default no-arg constructor in our ViewModel which is now missing. What we need to do create our own IocModelBinder that overrides the DefaultModelBinder to support constructor injection.
public class IocModelBinder<T> : DefaultModelBinder
{
public IocModelBinder()
{
Type bindedType = typeof(T);
var modelTypes = from t in AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
where bindedType.IsAssignableFrom(t)
select t;
foreach (Type type in modelTypes)
{
IocHelper.Container().AddComponentLifeStyle(type.FullName,
type, LifestyleType.Transient);
}
}
protected override object CreateModel(ControllerContext controllerContext,
ModelBindingContext bindingContext, Type modelType)
{
return IocHelper.Container().Resolve(modelType);
}
}There are other steps required to hook up the Castle Windsor IOC Container which are covered in part 1, including the implementation for the IocHelper class, and an explanation of what the constructor is doing. We're basically overriding the CreateModel method which does the actual 'new'ing up of the ViewModel object on binding.
To save having to add each ViewModel to the IOC Container's .xml configuration, just make sure your ViewModels extend a base class:
public class ProductForm : ViewModelBase { }Then register the IocModelBinder in the Global.asax.cs file:
public class MvcApplication : System.Web.HttpApplication
{
// ...route registration code (snip)... //
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ControllerBuilder.Current.SetControllerFactory(new IocControllerFactory());
ModelBinders.Binders.DefaultBinder = new IocModelBinder<ViewModelBase>();
}
}We now have another problem, because of the way we've set the DefaultBinder to IocModelBinder<ViewModelBase>(); we're now only able to bind types that derive from ViewModelBase, trying to bind any other type results in an exception. We could keep ASP.NET MVC's DefaultModelBinder and just add the IocModelBinder to the Binders collection, but we can't use base classes with this approach so we end up with something like the following:
ModelBinders.Binders.Add(typeof(ProductForm), new IocModelBinder<ProductForm>()); ModelBinders.Binders.Add(typeof(CategoryForm), new IocModelBinder<CategoryForm>()); ModelBinders.Binders.Add(typeof(CustomerForm), new IocModelBinder<CustomerForm>()); ModelBinders.Binders.Add(typeof(ProductsView), new IocModelBinder<ProductsView>()); ModelBinders.Binders.Add(typeof(CustomerOrderView), new IocModelBinder<CustomerOrderView>()); // ...long list of ViewModel types (snip)... //
It's just a shame we can't do this:
ModelBinders.Binders.Add(typeof(ViewModelBase), new IocModelBinder<ViewModelBase>());
What we want is a way to add IocModelBinder just once for a base class, while still keeping DefaultModelBinder to default back to. Jeffrey Palermo introduced the concept a SmartModelBinder in his book ASP.NET MVC In Action (Manning). A SmartModelBinder stores a collection of ModelBinders and chooses which one to use based on the type, it can even allow using a base class.
public class SmartModelBinder : DefaultModelBinder
{
private readonly IFilteredModelBinder[] _filteredModelBinders;
public SmartModelBinder(params IFilteredModelBinder[] filteredModelBinders)
{
_filteredModelBinders = filteredModelBinders;
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
foreach (IFilteredModelBinder modelBinder in _filteredModelBinders)
{
if (modelBinder.IsMatch(bindingContext.ModelType))
{
return modelBinder.BindModel(controllerContext, bindingContext);
}
}
return base.BindModel(controllerContext, bindingContext);
}
}
public interface IFilteredModelBinder : IModelBinder
{
bool IsMatch(Type modelType);
}We need to modify our IocModelBinder slightly to implement the IFilteredModelBinder interface:
public class IocModelBinder<T> : DefaultModelBinder, IFilteredModelBinder
{
// ...constructor and CreateModel override (snip)... //
public bool IsMatch(Type modelType)
{
return typeof(T).IsAssignableFrom(modelType);
}
}We then register it in our Global.asax.cs like so:
ModelBinders.Binders.DefaultBinder = new SmartModelBinder
(
new IocModelBinder<ViewModelBase>(),
new IocModelBinder<SomeOtherBaseType>(),
new SomeOtherModelBinder<SomeType>()
);Not everyone will like the idea of making the ViewModel class responsible for data access, they would argue the ViewModels should remain as simple POCOs free of any logic, and that it could be breaking the Single Resposibility Principle. I can understand where they're coming from. However, I do like the approach as it keeps the ViewModel class completely self contained, decoupled, and still allows unit testing, so long as the technique isn't abused. I would personally only recommend injecting database dependencies (Repositories) into ViewModels for filling out form input elements such as drop down lists, lists of check boxes etc.
Bugatti Veyron - As a photo mosaic. (from the blog Photo Mosaic Generator - Fun Adventures With Silverlight )
Red Bull gives you wings....that generate huge amounts of downforce #F1
about 18 hours ago from Twitterrific.vampire { -webkit-box-shadow: none; -webkit-box-reflection: none; } #cssjokes
7:44 PM July 30th from Echofon@edhenderson lol, lets get a trending topic going - .gangster .wrapper { color: #000; width: 150%; text-decoration: bling; } #cssjokes
7:36 PM July 30th from Echofon@weblivz I think the petition should be resubmitted but with security stuff taken out, as that's what the response purely focused on
6:13 PM July 30th from Echofon@weblivz I still think Chrome Frame can come to the rescue here, still keep their old browsers + legacy systems, no retraining costs etc.
6:12 PM July 30th from Echofon