As my MVC projects mature and grow, the need to have some omnipresent, ambient model properties quickly emerge. The application no longer has only one dynamic pieced of data on the page: A sidebar with a shopping cart, some news flash on the side – pretty common stuff. The rub is that a controller is invoked in context of a single intended request. The rest of the data, even though it could be just as dynamic, is expected to appear on it's own. There are many solutions to this scenario. MVVM prescribes creating elaborate objects which expose your new data as a property on some uber-object with more properties exposing the "side show" ambient data. The reason I don't love this approach is because it forces fairly acute awareness of the view, and soon enough you have many MVVM objects laying around, and views have to start doing null-checks in order to ensure you really supplied all the values before binding to them. Ick. Just as unattractive is the ViewData dictionary. It's not strongly typed, and in both this and the MVVM approach someone has to populate these properties – n'est pas? Where does that live? With MVC2, we get the formerly-futures feature Html.RenderAction(). The feature allows you plant a line in a view, of the format: <% Html.RenderAction("SessionInterest", "Session"); %>
While this syntax looks very clean, I can't help being bothered by it. MVC was touting a very strong separation of concerns, the Model taking on the role of the business logic, the controller handling route and performing minimal view-choosing operations and the views strictly focused on rendering out angled-bracket tags. The RenderAction() syntax has the view calling some controller and invoking it inline with it's runtime rendering. This – to my taste – embeds too much knowledge of controllers into the view's code – which was allegedly forbidden. The one way flow "Controller Receive Data –> Controller invoke Model –> Controller select view –> Controller Hand data to view" now gets a "View calls controller and gets it's own data" which is not so one-way anymore. Ick.
I toyed with some other solutions a bit, including some base controllers, special view classes etc. My current favorite though is making use of the ExpandoObject and dynamic features with C# 4.0.
If you follow Phil Haack or read a bit from David Heyden you can see the general picture emerging. The game changer is that using the new dynamic syntax, one can sprout properties on an object and make use of them in the view. Well that beats having a bunch of uni-purpose MVVM's any day! Rather than statically exposed properties, we'll just use the capability of adding members at runtime. Armed with new ideas and syntax, I went to work:
First, I created a factory method to enrich the focuse object:
public static class ModelExtension
{
public static dynamic Decorate(this Controller controller, object mainValue)
{
dynamic result = new ExpandoObject();
result.Value = mainValue;
result.SessionInterest = CodeCampBL.SessoinInterest();
result.TagUsage = CodeCampBL.TagUsage();
return result;
}
}
This gives me a nice fluent way to have the controller add the rest of the ambient "side show" items (SessionInterest, TagUsage in this demo) and expose them all as the Model:
public ActionResult Index()
{
var data = SyndicationBL.Refresh(TWEET_SOURCE_URL);
dynamic result = this.Decorate(data);
return View(result);
}
So now what remains is that my view knows to expect a dynamic object (rather than statically typed) so that the ASP.NET page compiler won't barf:
<%@ Page Language="C#" Title="Ambient Demo"
MasterPageFile="~/Views/Shared/Ambient.Master"
Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
Notice the generic ViewPage<dynamic>. It doesn't work otherwise.
In the page itself, Model.Value property contains the main data returned from the controller. The nice thing about this, is that the master page (Ambient.Master) also inherits from the generic ViewMasterPage<dynamic>. So rather than the page worrying about all this ambient stuff, the side bars and panels for ambient data all reside in a master page, and can be rendered using the RenderPartial() syntax:
<% Html.RenderPartial("TagCloud", Model.SessionInterest as Dictionary<string, int>); %>
Note here that a cast is necessary. This is because although dynamic is magic, it can't figure out what type this property is, and wants you to give it a type so its binder can figure out the right property to bind to at runtime. I use as, you can cast if you like.
So there we go – no violation of MVC, no explosion of MVVM models and voila – right?
Well, I could not let this go without a tweak or two more. The first thing to improve, is that some views may not need all the properties. In that case, it would be a waste of resources to populate every property. The solution to this is simple: rather than exposing properties, I change d the factory method to expose lambdas - Func<T> really. So only if and when a view accesses a member of the dynamic object does it load the data.
public static class ModelExtension
{
// take two.. lazy loading!
public static dynamic LazyDecorate(this Controller c, object mainValue)
{
dynamic result = new ExpandoObject();
result.Value = mainValue;
result.SessionInterest = new Func<Dictionary<string, int>>(() => CodeCampBL.SessoinInterest());
result.TagUsage = new Func<Dictionary<string, int>>(() => CodeCampBL.TagUsage());
return result;
}
}
Now that lazy loading is in place, there's really no reason not to hook up all and any possible ambient property. Go nuts! Add them all in – they won't get invoked unless used.
This now requires changing the signature of usage on the ambient properties methods –adding some parenthesis to the master view:
<% Html.RenderPartial("TagCloud", Model.SessionInterest() as Dictionary<string, int>); %>
And, of course, the controller needs to call LazyDecorate() rather than the old Decorate().
The final touch is to introduce a convenience method to the my Controller class , so that the tedium of calling Decorate() everywhere goes away. This is done quite simply by adding a bunch of methods, matching View(object), View(string,object) signatures of the Controller class:
public ActionResult Index()
{
var data = SyndicationBL.Refresh(TWEET_SOURCE_URL);
return AmbientView(data);
}
//these methods can reside in a base controller for the solution:
public ViewResult AmbientView(dynamic data)
{
dynamic result = ModelExtension.LazyDecorate(this, data);
return View(result);
}
public ViewResult AmbientView(string viewName, dynamic data)
{
dynamic result = ModelExtension.LazyDecorate(this, data);
return View(viewName, result);
}
The call to AmbientView now replaces any call the View() that requires the ambient data. DRY sattisfied, lazy loading and no need to replace core pieces of the MVC pipeline. I call this a good MVC day.
Enjoy!