Subscribe to this Blog

Subscribe to this Blog

ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls - Stephen Walther on ASP.NET MVC

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

clip_image002

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 &copy; 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

clip_image004

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.

Published Tuesday, August 12, 2008 10:47 AM by swalther
Filed under: , ,

Comments

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Tuesday, August 12, 2008 4:51 PM by Paco

I don't know any non MVC violating solution either, but why didn't you consider using HtmlHelper.RenderAction?

<% Html.RenderAction<Controller>(c => c.Action()); %>

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Tuesday, August 12, 2008 5:27 PM by LarryB

If you know that the data is going to be needed in every single page why not use the old standby of getting the data from the global.asax and placing it in the context items collection, then create yourself a viewdata base model with a property that retrieves that data from the items collection. Add that base to the generic on the master page and inherit all other view data models from that base?  Then you have the data available at every request. Just a thought.

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Tuesday, August 12, 2008 5:37 PM by swalther

@Paco -- RenderAction() was moved into the Microsoft.Web.MVC namespace after the last release (that means that it will most likely be in futures and not the core MVC framework).

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Tuesday, August 12, 2008 7:17 PM by Jeffrey Palermo

You have illustrated the need for subcontrollers.  HomeController should use a subcontroller for the partials.  Each action can use a different subcontroller specific to the needs of the action.

You don't want an inheritance hierarchy to attempt to solve a composition problem.

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Tuesday, August 12, 2008 8:41 PM by snidersh

I would definitely have included RenderAction, even though it is in the 'Futures' at the moment, especially if including a detailed instruction and code sample of how to do it in code-behind.  There's no 'good' way to do this at all right now, but RenderAction is a pretty decent 'not-good' way to do it.  The abstract base class method is a mess and could grow out of control as your application gets more complex.

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Wednesday, August 13, 2008 2:18 AM by Josh

That is a bit of work for something that should be simple. I agree subcontrollers would be a solution the best solution, and would still be testable.

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Wednesday, August 13, 2008 10:16 AM by Ajay

RenderAction is the best possible way to do this. It helps keep design and code much simple and modular. I strongly feel that no other way is as elegant as RenderAction.

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Wednesday, August 13, 2008 10:25 AM by Jamie

I think the problem some people have with RenderAction is that it's not sufficiently "MVC" because you have the view calling back to a controller.  While I'd agree that isn't strictly MVC, I don't think you're going to get much better.

Besides, it's kind of the same way of thinking that you'd use with an AJAX app.  In an AJAX app, you'd have client-side code firing up a controller action and then stuffing the result in a DIV or getting a JSON result and programmatically changing the UI.  Bottom line is that in this case you also have UI code calling back to a controller.  Granted, it's generally in response to a user action, but it's still UI code using a controller...

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Wednesday, August 13, 2008 12:12 PM by gunteman

"You don't want an inheritance hierarchy to attempt to solve a composition problem." - Jeffrey Palermo

Exactly!

"I think the problem some people have with RenderAction is that it's not sufficiently "MVC" because you have the view calling back to a controller.  While I'd agree that isn't strictly MVC, I don't think you're going to get much better." - Jamie

I get that feeling too. If we would have to add an extra "page composition layer" to the mix, I'm afraid the simplicity/beauty of the MVC framework would soon be lost.

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Wednesday, August 13, 2008 11:15 PM by Nathan

I would solve this problem using interceptors attached to my controller methods by an IoC container.

That is similar to using the ActionFilter, but the interceptors are less intrusive imo.

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Thursday, August 14, 2008 12:22 PM by Richard de Zwart

I love the way you go tru different options to find a solution. It teaches me a lot about the way you can do stuff in MVC. But I agree with Jeffrey that this problem is mostly a composition problem. Inheritance implements an "is-a" relation and I think your HomeControllerPopular is-not-a HomeControllerFeatuerd. Also inheritance very tightly couples classes and makes your solution less flexible.

I think you should really have the ApplicationController and a HomeController that inherits from that. But then the HomeController should delegate some of its work to composites like the FeaturedController. Makes sense?

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Thursday, August 14, 2008 4:22 PM by swalther

@Jeffrey, @Richard - You are absolutely right about the central issue being compositionality and inheritance being a bad approach to solving it.

I still have mixed feelings about the subcontroller approach. You take a trip from Seattle to Paris and then you phone home to have each of your bags sent to you? It seems like a better idea (less fuel, more scalable) to bring all of your luggage on the initial plane ride.

Also, the subcontroller approach couples the design of your view to the design of your controllers. Creating a new user control requires you to create a new controller action. It would be nice if your page design could be completey independent from the architecture of your application.

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Thursday, August 14, 2008 4:44 PM by Duarte

Great post! I really like the first solution and that's the one I've been using. Since the problem relates to testability, it is always tackled by a little mangling in the tests. In this case it would suffice to mock ActionExecutingContext and explicitly call the OnActionExecuting method. That's how I started out until I found PostSharp (an AOP framework for .NET, in case you're not aware): Partial becomes an aspect of the action. It even fits better conceptually, since there's no actual filtering involved when using ActionFilterAttribute, but injection.

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Sunday, August 17, 2008 5:20 PM by swalther

@Ernest - RenderAction() replaces RenderComponent() and RenderAction() will be in the futures download. So you can use RenderAction() with the warning that it is not supported by Microsoft.

The comments that I received on this post convinced me that using an action filter (method number #3) is the right way to deal with the scenario that you describe. You can create an action filter that injects the view data that you need for your user control. Decorate each controller action that returns a view that contains the user control with the action filter.

Hope this helps!  

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Wednesday, August 20, 2008 11:44 PM by Paul Ascher

@LarryB - I think we're at the same wave length, I've been taking this exact approach ever since I started on mvc.net, it's easy to write and read, and, hmmm, well, I just like it better than all other solutions I've seen so far.

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Friday, August 22, 2008 1:20 AM by joshka

Your final solution is begging to replaced with a set of decorators. Could you substitute a decorator pattern wired up in your IControllerFactory instead of the static class inheritance?

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Friday, August 22, 2008 9:42 AM by vdh_ant

CALL TO ACTION: Master pages need some love

forums.asp.net/.../1309375.aspx

Please go to the above and leave a comment if you think master pages need some love...

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Monday, September 1, 2008 8:36 AM by Alexander

Stephen, great stuff and has definitely helped me a lot to catch MVC in immense details... Do you have code download for all of your samples? For example this tip31 doesn’t have download associated to it  or is it in purpose to left out?

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Monday, September 22, 2008 2:58 PM by Nick

While your solution works, it's still more work than I think it should be.  Why not have the ability to concatenate views?  That way, one could have individual views for the header, content, and footer.

# re: ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls

Monday, October 13, 2008 2:11 PM by Baldev Rawat

Stephen, Can u please provide a sample project to download. I want to see the implementation. I am not able to implement this tip. It will be very nice you post some live example here.