ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls
In this tip, I discuss four strategies for passing data to Master Pages and User Controls. I explain how you can pass data by using a code-behind class, by using an action filter, by using method calls, and by using abstract controller base classes. I recommend the final strategy.
In this tip, I recommend a method for passing data to your master pages and user controls. However, before I make my recommendation, I first survey a number of alternative solutions to the same problem.
The Problem
Imagine that you are building a movie database application with the ASP.NET MVC framework. You decide that you want to display the list of movie category links on each and every page in your application. That way, users of the application can quickly navigate to their favorite movie category. Since you want the movie categories to appear on every page, it makes sense to display the categories in your application’s master page.
You also decide to display a list of “featured” movies on some pages, but not all pages. The list of featured movies is retrieved from the database randomly. You decide to implement the featured movies with a user control: the FeaturedMovies control (see Figure 1).
Figure 1 – The Movie Database Application
Here’s the problem. You need to pass the list of movie categories to your master page for every page in your application. You need to pass the list of featured movies to your user control for certain pages in your application. How do you do it?
Using a Code-Behind Class
The most tempting, but wrong, solution to this problem is to retrieve the data in the code-behind class for your master page and FeaturedMovies user control. The master page in Listing 1 displays all of the movie categories by retrieving the list of categories from a property of the code-behind class named Categories.
Listing 1 – Site.Master
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="Solution1.Views.Shared.Site" %> <%@ Import Namespace="Solution1.Models" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> <title>Movies</title> <link href="../../Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div class="page"> <div id="header"> <h1>Movie Database Application</h1> </div> <div id="main"> <div class="leftColumn"> <ul> <% foreach (MovieCategory c in this.Categories) { %> <li> <%= Html.ActionLink(c.Name, "Category", new {id=c.Id} )%></li> <% } %> </ul> </div> <div class="rightColumn"> <asp:ContentPlaceHolder ID="MainContent" runat="server" /> </div> <br style="clear:both" /> <div id="footer"> Movie Database Application © Copyright 2008 </div> </div> </div> </body> </html>
The code-behind class for the master page is contained in Listing 2. Notice that the code-behind class accesses the MovieDataContext directly. All of the movie categories are retrieved by performing a LINQ to SQL query against the MovieDataContext.
Listing 2 – Site.Master.cs
using System.Collections.Generic; using System.Linq; using Solution1.Models; namespace Solution1.Views.Shared { public partial class Site : System.Web.Mvc.ViewMasterPage { protected IEnumerable<MovieCategory> Categories { get { var dataContext = new MovieDataContext(); return from c in dataContext.MovieCategories select c; } } } }
You could do the exact same thing with the FeaturedMovies user control. Use the FeaturedMovies code-behind class to access the DataContext directly and retrieve a list of featured movies from the database.
So, why is this wrong? This certainly seems like a straightforward solution. It works, why complain?
The problem with this solution is that the code in the master page code-behind class is not testable. You cannot easily write unit tests for the Site class because the Site class inherits from the ViewMasterPage class which inherits, in turn, from the Page class. The Page class relies on the HTTP Context object and all hope of isolating your code so that it can be tested goes away.
You should strive to avoid ever using a code-behind class for your application logic when writing an ASP.NET MVC application. Try to push everything back down into your controllers. Controllers are designed to be testable.
Using an Action Filter
So let’s try to solve the problem of passing data to a master page or view in another way. In this section, we create an action filter that modifies the view data passed to a view. The idea is that you can decorate your controller actions with one or more of these action filters to control what view data is passed from the controller to the view.
The action filter, named [Partial] is contained in Listing 3.
Listing 3 – ActionFilters\PartialAttribute.cs
using System; using System.Reflection; using System.Web.Mvc; namespace Solution2.ActionFilters { public class PartialAttribute : ActionFilterAttribute { private string _partialClassName; public PartialAttribute(string partialClassName) { _partialClassName = partialClassName; } public override void OnActionExecuting(ActionExecutingContext filterContext) { var viewData = (filterContext.Controller as Controller).ViewData; ExecutePartial(_partialClassName, viewData); } private void ExecutePartial(string partialName, ViewDataDictionary viewData) { // Get partial type var partialType = Type.GetType(partialName, true, true); var partial = Activator.CreateInstance(partialType); // Execute all public methods var methods = partialType.GetMethods(); foreach (MethodInfo method in methods) { var pams = method.GetParameters(); if (pams.Length > 0 && pams[0].ParameterType == typeof(ViewDataDictionary)) method.Invoke(partial, new object[] { viewData }); } } } }
When you add the [Partial] action filter to a controller action, the action filter adds additional data to the view data. For example, you can use the [Partial] attribute to add the movie categories to view data so that the categories are available in the master page. You could also use the [Partials] attribute to add featured movies to the view data so that this data is available for the FeaturedMovie user control.
The [Partial] attribute takes a class names, instantiates the class, and executes all of the public methods of the class (each method that take a ViewDataDictionary parameter). The controller in Listing 4 illustrates how you can use the [Partial] action filter to modify the view data returned by different controller actions.
Listing 4 – HomeController.cs
using System.Linq; using System.Web.Mvc; using Solution2.ActionFilters; using Solution2.Models; namespace Solution2.Controllers { [Partial("Solution2.Partials.Master")] public class HomeController : Controller { [Partial("Solution2.Partials.Featured")] public ActionResult Index() { return View(); } public ActionResult Category(int id) { var dataContext = new MovieDataContext(); var movies = from m in dataContext.Movies where m.CategoryId == id select m; return View("Category", movies); } } }
Notice that the HomeController class itself is decorated with the [Partial] action filter. Because the [Partial] action filter is applied to the class, the action filter executes whenever any of the HomeController action methods are called. Applying the [Partial] attribute at the class level makes sense when supplying view data for a master page.
The class-level [Partial] attribute adds the movie categories to view data. The [Partial] executes the methods of the Solution2.Partials.Master class which is contained in Listing 5.
Listing 5 – Master.cs
using System.Linq; using System.Web.Mvc; using Solution2.Models; namespace Solution2.Partials { public class Master { public void AddViewData(ViewDataDictionary viewData) { var dataContext = new MovieDataContext(); var categories = from c in dataContext.MovieCategories select c; viewData["master"] = categories; } } }
The AddViewData() method adds the categories to a key named master in the view data dictionary. The categories are retrieved from the view data in the master page and displayed.
The [Partial] attribute can be applied to only certain action methods and not others. For example, the Index() method in Listing 4 includes a [Partial] attribute that executes the Solution2.Partials.Featured class. This class adds the data for the FeaturedMovies user control.
So what’s wrong with this solution to the problem of passing data from a controller to a master page or user control? The advantage of this approach over the previous approach is that we have managed to push the logic for retrieving the database data back down into the controller. The view data is modified when a controller action is invoked.
Also, this solution is nicely compositional. By using the [Partial] attribute, you can layer more and more view data into the view data dictionary. For example, if you discover that you need to add a new user control to certain pages, and the new user control needs a different set of data, you can simply add a new [Partial] attribute to the right controller actions and add the new data to the view data dictionary.
Unfortunately, and this is a big unfortunately, this solution is not very testable. The action filters don’t get executed when you call action methods within a unit test. Therefore, we need to look for a different strategy.
Calling Partial Methods Directly
Let’s move on to solution number three in our quest to solve our problem. In this section, we attempt to solve the problem of passing data to a master page or user control by explicitly coding the logic to retrieve the data into our controller actions. Our modified HomeController is contained in Listing 6.
Listing 6 – HomeController.cs (with partials logic)
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using Solution3.Models; using Solution3.Partials; namespace Solution3.Controllers { public class HomeController : Controller { public HomeController() { Master.AddViewData(this.ViewData); } public ActionResult Index() { Featured.AddViewData(this.ViewData); return View(); } public ActionResult Category(int id) { var dataContext = new MovieDataContext(); var movies = from m in dataContext.Movies where m.CategoryId == id select m; return View("Category", movies); } } }
Notice that the HomeController in Listing 6 now has a constructor. The Master.AddViewData() method is called in the constructor to modify the view data returned by any of the controller actions. This method adds the view data that is displayed in the master page.
The Index() action also has been modified. Within the Index() action, the Featured.AddViewData() method is called. This method adds the view data necessary for the FeaturedMovies user control. Because the FeaturedMovies user control is contained in the Index view and not the Category view, it does not make sense to call the Featured.AddViewData() method in the constructor.
The advantage of this solution is that it is very testable. When you call the Index() method, the view data is modified by both the Master and Featured partial methods. In other words, you can easily test whether or not your view data contains the right data for the master page and the FeaturedMovies user control.
So, what’s wrong with this solution? All of the logic for adding the view data is contained in the controller classes. This solution is much better than the previous two solutions. The only problem with this solution is that it violates the Single Responsibility Principle.
According to the Single Responsibility Principle, code should have only a single reason to change. However, we have many reasons to change the Index() method in Listing 8. If we ever decide to add a new user control to the Index view, and the new user control displays a new set of data, then we will need to change the Index() action.
The intent behind the Single Responsibility Principle is that you should never change code that works. Changing code always introduces the possibility of creating a bug in your application. We need to find some way to add new view data to the view data returned by a controller action without modifying our controller actions.
Using Abstract Base Classes
Here’s my final solution to the problem of passing data to master pages and user controls: We’ll use abstract base classes to modify the view data returned by our controller actions. I’ll warn you right now that it is complicated. We are required to build a lot of classes. However, each class has a single responsibility. Each class is responsible for just one type of view data (see Figure 2).
Figure 2 – Class Hierarchy
We’ll create one abstract base class, named ApplicationController that modifies the view data dictionary by adding all the view data required for our master page (see Listing 7). The ApplicationController is used as the base class for every controller in our application and not just the HomeController.
Listing 7 – ApplicationController
using System.Web.Mvc; using Solution4.Partials; namespace Solution4.Controllers { public abstract class ApplicationController : Controller { public ApplicationController() { Master.AddViewData(this.ViewData); } } }
Next, we’ll create an abstract base class named HomeControllerBase (see Listing 8). This class contains all of the application logic that normally appears in the HomeController class. We’ll override the action methods in this class to add the additional view data that we need for particular user controls.
Listing 8 – HomeControllerBase.cs
using System.Linq; using System.Web.Mvc; using Solution4.Models; namespace Solution4.Controllers.Home { public abstract class HomeControllerBase : ApplicationController { public virtual ActionResult Index() { return View("Index"); } public virtual ActionResult Category(int id) { var dataContext = new MovieDataContext(); var movies = from m in dataContext.Movies where m.CategoryId == id select m; return View("Category", movies); } } }
For each user control, we’ll need to create an additional abstract class. For the FeaturedMovies user control, we’ll create a HomeControllerFeatured class (see Listing 9). For the PopularMovies user control, we’ll create a HomeControllerPopular class (see Listing 10).
Listing 9 – HomeControllerFeatured.cs
using System.Web.Mvc; namespace Solution4.Controllers.Home { public abstract class HomeControllerFeatured : HomeControllerBase { public override ActionResult Index() { var result = (ViewResult)base.Index(); Partials.Featured.AddViewData(result.ViewData); return result; } } }
Listing 10 – HomeControllerPopular.cs
using System.Web.Mvc; namespace Solution4.Controllers.Home { public abstract class HomeControllerPopular : HomeControllerFeatured { public override System.Web.Mvc.ActionResult Category(int id) { var result = (ViewResult)base.Category(id); Partials.Popular.AddViewData(result.ViewData); return result; } } }
Finally, we need to put one last class at the top of this layer cake. We’ll create the HomeController class itself. This class simply inherits from one of the base classes (see Listing 11). It should contain no application logic itself. It is just acting as the public face of all of the other classes.
The HomeController class is the only class in this hierarchy that is not an abstract class. Because it is not an abstract class, its controller actions can be invoked by the world.
Listing 11 – HomeController.cs
namespace Solution4.Controllers.Home { public class HomeController : HomeControllerPopular { } }
Right now, you might be feeling overwhelmed over the number of classes. However, the advantage of this approach is that we have cleanly separated out the logic to create the view data. Each abstract class has a single responsibility. Our code is not fragile.
Summary
I’m not completely convinced by my own tip. I’m still tempted to use action filters to add view data for my master pages and user controls. The solution described In the last section, using abstract base classes, seems like a lot of work. I’m curious about how others have solved this problem.