Tips for Managing ASP.NET MVC Views
Feb 25 2009After working on an ongoing ASP.NET MVC project for a couple of months I’ve learned a couple of lessons when it comes to dealing with Views. Keep as much logic out of the views as possible! This can be tricky because it’s so easy to let code sneak into your views. But by following the tips below you’ll be able to keep your logic organized and views clean.
1) Follow the Rails pattern of having a Helper class for each controller. The helper class deals with html snippets or formatting functions on a per controller level.
Pushing out bits of code like date formatting into helper classes not only cleans up the views, but aids in testability. Logic is consolidated in one place simplifying maintenance. Creating Helpers on a per controller level also creates a namespace where functionality can be found intuitively.
2) Don’t overload the Html helper with one-off functions. Organize functions into their respective helper classes.
It’s so easy to want to add extension methods to the HtmlHelper class. It’s like a siren crying out “do it, do it”. This is bad! Extension methods are good for some things, but can easily garble an API. The HtmlHelper class should be reserved for rendering core html elements. As an alternative, leverage helper classes you create on a per-controller basis.
//These shouldn't belong to HtmlHelper! public static string ShowSomethingOnlyForHomeController((this HtmlHelper helper) ... public static string RenderDateTimeNowInSpan(this HtmlHelper helper) ...
3) Leverage partial views for iterative content, even if it’s not reused elsewhere (hint: it probably will be eventually).
So instead of:
<%= foreach (var photo in photos) { %><%= photo.Name %><%= } %><%= photo.Description %>
<%= photo.Date %>
use:
<% foreach (var photo in photos) { %> <% Html.RenderPartial("~/Views/Photos/PhotoTemplate", photo); %> <% } %>
This will greatly minimize the amount of markup scattered throughout a view, keeping view files focused on a specific task (just like good class design). It’ll also make version control easier because changes will be isolated to their respective file, allowing someone to better see what changes happened where. It also eliminates excessive merging.
4) Organize partial views within their respective controller, not a shared folder- even if it’s a global skin.
Dumping partials within a shared folder can cause overcrowding and jumbling in the long term. Prefer organization and grouping of related content (again, just like good class design).
5) Prefer a strongly typed view and leverage specialized ViewData types for aggregating random models under one root.
It can be easy to dump stuff into the ViewData hash. However, prefer using a custom ViewData class instead. It’s easier to know what data is available for the view. This is a lot more intuitive than rummaging through a controller, which happens when teams share code. Also leverage the null object pattern for properties to avoid having to do null reference checks in the views.
Instead of:
ViewData["Title"] = "My Photos"; ViewData["Photos"] = myPhotos; ViewData["User"] = currentUser; return View();
use:
//Title and User can be properties of a base view data class. var vd = new PhotoListViewData() { Photos = myPhotos, Title = "My Photos", User = currentUser }; return View(vd); //Sample null object pattern (always returns a valid object, so no if null or Count == 0): private List<Photo> _photos = new List<Photo>(); public List<Photo> Photos { get { if (_photos == null) _photos = new List<Photo>(); return _photos; } set { _photos = value; } }
6) Minimize code snippets in views.
Code snippets occur in many ways. They can be as simple as formatting a date, to string concatenation, to even doing a grouping/projection to get data correct. Doing this in a view easily leads to bugs and isn’t testable. Any data processing specifically for views should occur in the controller action, custom ViewData object itself, or in a helper class. Code in views should be simplified to looping, calling a property, or calling a single method. Anything more than that will get you into trouble!
Leverage RenderAction or MvcContrib’s SubController for dealing with shared isolated functionality not relevant to a view.
Unfortunately, as of RC1 there still isn’t a great way to deal with aggregating disparate content in an action. You’ll need to resort to the RenderAction in the Future’s dll or use MvcContrib’s SubController. The point is the same- keep actions specific to what you’re doing. If you need to aggregate disparate content in a view (like a menu in the header, or a shopping cart) offload functionality into an action and call RenderAction. Having actions do multiple, random things leads to messy code. Prefer a single point of entry into supporting view content.
Good luck and share your tips!