Bandage - Add properties to view models at runtime
This post introduces the Bandage project for ASP.NET MVC.
Introduction
Are you a pragmatic ASP.NET MVC web developer? I like to think I am. When working with simple entities it feels like overkill to use separate view model objects in my views. I'd rather just send my entity directly to the view and get on with something more fun (like delivering business value!)
Whilst AutoMapper does a great job of conventionally mapping entities into view models, I'd rather not have to create the view model class when it's essentially a property-by-property mirror of my entity.
Nothing stops me sending entities directly to the view. However, things get messy when I need to display information derived from properties of the entities. I end up resorting to helpers, extension methods and other contraptions that don't feel as natural as simply getting a property value. Furthermore, when using dynamic view models, extension methods do not work, so they're no good anyway.
If I later decide to man-up and create real view models, I'd then have to replace all helper calls in my view with properties. Have you tried refactoring view code? It's not fun!
I created the Bandage library to solve this problem. Read on for a simple example and learn what Bandage can do for you.
Use Case
Let's take a use case I hit quite often when developing MVC web applications. We have some list of objects, products for example, to show on the screen. For each product, we want a hyperlink to a details page.
class Product {
public int Id { get; set; }
public string Name { get; set; }
}
The URL for a product details page is formed using the Id and the Name properties. The Name property will be processed to create a "slug" (a URL safe version of the actual Name). This is to be more SEO-friendly. We keep the Id in the URL to keep our coding easy.
For example: The product
new Product { Id = 1, Name = "Interwebz for Dummies" }
will have the URL /product/1/interwebz-for-dummies
My MVC route set up defines the route as follows:
routes.RouteUrl(
"ProductDetails",
"product/{id}/{slug}",
new { controller = "Product", action="Details" }
);
The ProductController List action will get the products and send them to the view like this:
public ActionResult List() {
ViewModel.Products = ProductRepository.GetAll();
Return View();
}
The List view is then something like:
<ul>@foreach (Product product in View.Products) {
<li><a href="Url.RouteUrl("ProductDetails", new { id = product.Id, slug = Util.GetSlug(product.Name) })">@product.Name</a></li>
}</ul>
Code like that, in my opinion, does not belong in the view. It's complex and really doesn't express the intent well.
It would be so much nicer to write the following.
<ul>@foreach (var product in View.Products) {
<li><a href="@product.Url">@product.Name</a></li>
}</ul>
We simply need a way to add an extra property to Product. We don't want the Url property in our business object, it's purely a web-thing. We don't want to create a view model, that's overkill for simple situations like this.
Notice that the product variable in the second code snippet is dynamically typed. This is because View is dynamic, so anything you get from it is also typed as dynamic. This dynamism is where we can inject some magic pixie dust.
Say hello to Bandage
Bandage is a small library which lets you define dynamic view models and then effectively monkey patch properties onto existing types. So, for our example, we'll have Product objects in the view model and then add a Url property to each product.
The controller List action changes from before to this:
public ActionResult List() {
var viewmodel = new Bandage.DynamicViewModel();
viewmodel.Products = ProductRepository.GetAll();
viewmodel.Add(DynamicProperty.For<Product>("Url", product => ProductUrl(product)));
Return View(viewmodel);
}
private string ProductUrl(Product p) {
return Url.RouteUrl("ProductDetails", new { id = product.Id, slug = Util.GetSlug(product.Name) });
}
The key line of code there is when we add a DynamicProperty for the Product type. We provide the name of the property and a lambda that will return the value for a given Product. This is registered for the entire view model. So any Product object, anywhere in the object graph, will have this dynamic property available.
The view can now use this dynamic view model and its magic dynamic properties.
<ul>@foreach (var product in Model.Products) {
<li><a href="@product.Url">@product.Name</a></li>
}</ul>
Note: We are passing the viewmodel in to the view's Model property, rather than using the View object as before. Model is also typed as dynamic however in this case. This is because Controller's ViewModel property is readonly so we can't set it to our own object. Please vote on the CodePlex issue to have this fixed.
Behind the curtain
DynamicViewModel intercepts any attempt to get a property and returns a special wrapper object instead of the original value. This wrapper is also dynamic and intercepts property calls and other kinds of member access.
When we ask for the Url property of Product, the wrapper first checks if a DynamicProperty added to the view model can be used instead. The product object is passed to the lambda we register in the controller. This then returns the value.
All other regular member access is delegated back to the wrapped object.
Try Bandage for yourself
Bandage is an open source library (MIT License). You can get the source code at GitHub. Feel free to fork, improve and send me pull requests.
The dynamic object wrapper is still beta-quality and needs some serious testing. However it does work for my meagre needs.
I'd love to hear what you think about Bandage. Give me a shout on twitter @andrewdavey or leave a comment here.