Skip navigation

Dealing with Data Shared Among Views

Explore approaches to injecting global data into ASP.NET MVC views

In ASP.NET MVC, a controller action normally cares about performing a particular task and about the subsequent view. The view rendered to the user will likely contain some of the data generated by the controller action, but it can also include other data globally shared by multiple views in the same application. Quite often, global data displayed in the view is stored or retrieved by master pages and doesn’t require any special effort on your part to be displayed. However, many situations occur in which you don’t use master pages, or you employ a view engine that doesn’t support anything like master pages, or you decide to keep global data out of the master page template. Typical examples of globally shared view data are menus, images, sidebars, summary information, and breadcrumbs. If this information is static, you can easily stuff it in a master page; in other cases, this information has to be retrieved from a database or a cache and made available to the view in some way.

In ASP.NET MVC, the view is not supposed to perform any actions such as retrieving data, writing data, or anything else. As a result, the view is expected to receive from the outside any data it has to display. This includes data specifically calculated by the controller action that triggers the view, as well as global data shared by multiple views. How should the view retrieve global data? Should this be a responsibility of the view? Should the controller pass this data piecemeal? Or should you define a global container that the view can access autonomously? In this article, I’ll explore approaches to this fairly common dilemma of ASP.NET MVC development.

Using a Global Container

At its core, the dilemma about shared data and ASP.NET MVC views centers on whether the controller should retrieve its own context-specific data or the entire data set that the view needs. A controller that incorporates the additional logic required to retrieve global data for the view it triggers is on the borderline between good code and code that violates the Single Responsibility Principle. Put another way, a controller method that does its job and also retrieves data for the view probably should be coded differently. And this may not be acceptable from a design perspective.

I see how a controller action should be responsible for ensuring that the view gets its entire data set. On the other hand, I also see how incorporating additional logic for global data gives the controller excessive responsibility. Is there a middle ground?

I suggest an approach that entails the creation of a global class—the Registry—that contains properties and methods to be considered global and therefore accessible from any views. In my vision, the Registry is an application-specific class whose programming interface depends on the application. The code in Figure 1 provides a possible skeleton for such a global class.

The code presents an application-wide class. Rest assured that if you like the approach, you can create many such classes to segregate the interface, perhaps even one registry per controller or one per master page.

The Registry class is not the only part of the application you might want to have available from various places. You can store the Registry in a brand new ApplicationContext class along with other dependencies and global objects. Figure 2 shows a possible implementation for the ApplicationContext class. You access the registry for reading and writing by using the following expression:

ApplicationContext.Registry

A controller method can load data into a strongly typed view as follows:

var model = new CustomerViewModel();
model.Countries = ApplicationContext.Registry.GetCountries();
model.Customers = customers;

In this way, the controller is not directly responsible for the code that retrieves the entire data set to show in the view. The code that retrieves global data is invoked from the controller but through a global interface that is simply consumed by the controller. Finally, the SetRegistry method in Figure 2 keeps the code testable because it allows you to add any mock you need for the registry object.

Using an Action Filter

The thinking that brought me to the aforementioned solution is that global data must be treated through global objects. The controller is responsible for passing data to the view; if the view needs global data shared by multiple views, this is transparent and the controller must pass this data in. At the same time, it's reasonable that the controller doesn’t have to include the retrieval code itself because this code can easily be replicated across multiple controllers. For this reason, I initially opted for a global class that contains the retrieval logic.

Anyway, everyone would likely agree that keeping code for global data retrieval out of the controller class is a good thing. Is there another, and maybe cleaner, way of achieving the same result? Action filters is an option that deserves further exploration. An action filter is a class that implements one or more interfaces that add a new behavior on top of a controller method. Each action filter interface identifies a set of methods that execute at some well-known points in the call stack for a controller action. By writing action filters, you can add your own code before or after the execution of the controller action. For example, you can write an AddSharedData action filter that adds global data to the ViewData collection or, better yet, to the ViewData model object, if you’re dealing with strongly typed views. If you opt for this solution you have no need to add anything to the controller class except an attribute to decorate the method, as shown below:

\\[AddSharedData\\]
public virtual ActionResult Index(string customerId)
\\{
    :
\\}

Figure 3 illustrates a sample action filter for adding shared data. The attribute class derives from ActionFilterAttribute and overrides the method OnActionExecuted. The overridden method is invoked right after the execution of the controller method. All it does is grab any global data that views will share and adds it to the ViewData collection.

A couple of weak points of this implementation are that you're forced to use ViewData instead of strongly typed views. Furthermore, in using the ViewData dictionary you also are forced to use magic strings to name dictionary slots. (Well, to be precise you're not strictly forced to use explicit strings because you may opt for named constants.)

Is there anything you can do to use a strongly typed view from within the action filter? The biggest problem here is that you don’t know the type of the view model object. Because you miss this piece of information, you can’t cast ViewData.Model to it and can’t directly access public members of the view model object. However, you use this kind of action filter when you have global data to share across multiple views. Because the data is shared, we can conclude that it can be represented through a common data structure, as in the example below:

public class SharedData
\\{
   IList<String> MenuItems \\{get; set;\\}
   IList<String> Countries \\{get; set; \\}
   :
\\}

The code in Figure 3 can now be rewritten as shown in Figure 4. It's assumed that any views invoked by any controller action methods decorated with the SharedData attribute are strongly typed and its view model inherits from the SharedData type. In other words, any view that contains global data builds its own view model by extending the SharedData type. In this way, the action filter just casts the ViewData.Model member to the type and fills it up. Needless to say, you get an exception if, for whatever reason, the view is not strongly typed.

If your application is compiled with Visual Studio 2010 against the .NET Framework 4, you can take advantage of the new features of C# 4, such as the dynamic keyword. The following code compiles and works just fine:

// Tells the compiler the variable model will be resolved at run time
dynamic model = filterContext.Controller.ViewData.Model;
model.Countries = AppContext.Registry.GetCountriesFromCache();
model.MenuItems = container.MenuItems = AppContext.Registry.GetMenuItems();
:

Using the dynamic keyword saves you the base class that would otherwise be required in .NET 3.5. However, in my opinion, the base class saves some run time burden and keeps the code cleaner and easier to read.

In C#4, the dynamic keyword tells the compiler not to resolve any expression where such a variable or member is involved. As a result, the compiler emits a special call to a new runtime component—the Dynamic Language Runtime—which will interpret the expression dynamically and resolve it using any object instance that is currently bound to the variable. As a result, the model.Countries expression is not evaluated at compile time if the model variable is declared as a dynamic type. The compiler emits code that will evaluate the model.Countries expression at run time. When this happens, the model variable is recognized as an instance of the view model type and successfully resolved as long as a Countries and a MenuItems member exist on the type.

Render Actions

Finally, to let the view access data shared with other view objects, an entirely different solution is possible in ASP.NET MVC 2. The solution is based on a new feature introduced with ASP.NET MVC 2—render actions. A render action is a special call made to a controller method that you place at rendering time from within a view. A render action takes a reference to a controller method, executes it, and places the resulting response in the view. In this way, the view calls back some code on a controller.

Whether this solution is acceptable depends on your perspective. A render action entails code that is invoked by the view. In ASP.NET MVC, a view is mostly a static object that renders what it explicitly retrieves from the controller. With render actions, you make your view a bit more active. By calling render actions you're certainly not violating the otherwise neat separation of concerns between controllers and views; however, your resulting view isn't as passive as it would be otherwise. The final choice is up to you.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish