Populating an ASP.NET MVC View with Globally Shared Data

Use an action filter to fill up view models with data shared across multiple views

About a year ago, I wrote an article for DevProConnections exploring ways to inject global data into ASP.NET MVC views (see "Dealing with Data Shared Among Views"  The solutions that I presented are, for the most part, common tricks. Recently, however, I needed to enhance those solutions to better address a problem in a larger application that had several master/detail views.

This problem isn’t unique to master/detail views but shows up clearly when you deal with them. When you write a master/detail view in Web Forms, you have only one concern: how to get key information for the item that is selected in the master. When you know how to do that, you can populate the detail view and render the page; you don't need to worry about refilling the master view. That's one benefit of the viewstate.

In ASP.NET MVC, you have no viewstate. In this context, the news isn’t great because it means that you need to be concerned with filling the detail view and refilling the master view. The issue isn’t really a coding problem; it's purely a design issue. But unresolved design issues never die—they come back unexpectedly as the size of the project grows.

The Design Issue

In ASP.NET MVC, each request results a controller conducting an action or, better yet, triggering a service component. The action relates to a specific user request, such as "show me the list of countries where the company has subsidiaries" or "show me details about this country." Ideally, you want to serve the former request by running code that gets only the list of countries, and serve the latter request by running code that gets only details about a given country. The issue is that ASP.NET MVC forces the code that serves the second request to also provide the list of countries (again). Otherwise, the drop-down list of countries will be empty when the detail view is shown (see Figure 1).

Figure 1: Improperly filled master view
Figure 1: Improperly filled master view

Figure 2 shows a sample controller class for the application that Figure 1 shows. The controller uses injected worker components to serve actions and get view model objects. (This approach is an emerging pattern for ASP.NET MVC applications, and I recommend it to everybody, but not for just any application. The approach is described thoroughly in Programming ASP.NET MVC 3, Microsoft Press, 2011.)

The worker service class does the job of populating view model objects, which the controller then hands to the view engine. Figure 3 shows a sample worker service. The first method in the listing returns a fully initialized view model object, which has the page title and a list of countries. The second method returns a view model object that sets the title and country details, but does not list countries.

Admittedly, you could just add an extra call to GetCountryViewModel to retrieve and store the list of countries. You wouldn’t experience the failure that is shown in Figure 1 and could leave the office early. But what if the amount of information that the multiple views share is large and comes from scattered sources? You would need that information in a variety of controllers (or worker services) for a variety of views. How many dependencies should you be passing around?

The idea is to leave the controller (or worker service) in charge of only the data that must be manipulated in that specific call context, and to move the remaining code elsewhere. An excellent place to move this extra code is to an action filter.

The ViewModelFiller Action Filter

Action filters are small chunks of behavior that you can attach to controller methods. In a way, an action filter is like an interceptor and can insert your code in four places—before and after the action is executed and before and after the view result is executed. For our purposes, we need an action filter that kicks in right after the action methods returns. Here's the skeleton of such a filter:

public class ViewModelFillerAttribute : ActionFilterAttribute

{

    public override void OnActionExecuted(ActionExecutedContext filterContext)

    {

        var model = filterContext.Controller.ViewData.Model;

 

        // Apply changes to the view model object

        ...

    }

}

There are a couple of issues with this approach. First, the Model property, as you retrieve it from the controller instance, is of type Object and needs casting to become usable. But to which type should you cast the property? Second, you probably need several action filters per project, depending on the composition of your views. For example, if you have a sidebar that shows an RSS feed, then the sidebar must be refreshed for each action that returns a view that incorporates it. Imagine adding an RSS sidebar and a few more action buttons to the view that Figure 1 shows. Each action that those buttons trigger must refresh the sidebar (and more). As you can see, you have a lot of repetitive code to organize in the best possible way.

At this stage, you shouldn't expect to have just one action filter: Each filter has dependencies on the view model type and data-retrieval logic. Let's tackle the view model type first.

A Hierarchy of View Model Types

All views in your ASP.NET MVC application have a common set of properties; define a base class out of them. For the sample application in Figure 1, the base class looks like this:

public class ViewModelBase

{

    public ViewModelBase()

    {

        BaseTitle = "Countries of the world";

    }

    public static String BaseTitle { get; private set; }

    public String Title { get; set; }

}

 

Each view model type is then derived from this class or from an intermediate class that groups the properties for a container area of the main view:

public class IndexViewModel : ViewModelBase

{

public IndexViewModel()

{

Countries = new List();

}

public IList Countries { get; set; }

}

 

In the sample application, you have three levels in the view hierarchy: the global view model, which contains the title; the view model for the home page, which adds Countries; and the view for the detail of a given country, which adds area, capital, and population:

public class CountryViewModel : IndexViewModel

{

public String Country { get; set; }

public String Area { get; set; }

public String Capital { get; set; }

public String Population { get; set; }

}

In this way, each controller action deals with only the view model properties that are specific to that action’s role, while having access to the base properties in case some override is necessary (i.e., changing the page title), as illustrated in Figure 3. The following code shows how to rewrite the action filter for a specific view model type:

public class ViewModelFillerAttribute : ActionFilterAttribute

{

    public override void OnActionExecuted(ActionExecutedContext filterContext)

    {

        var model = filterContext.Controller.ViewData.Model as IndexViewModel;

        // Apply changes to the view model object

        Model.Countries = CountryServices.GetCountries();

    }

}

 

The Details method is decorated as follows:

[ViewModelFiller]

public ActionResult Details(String country)

{

    var model = _service.GetCountryViewModel(id);    return View(model);

}

 

The net effect for this method is that the associated view model object, immediately after it returns, has an empty Countries property. But when the filter has done its job, all the data is in place, and the view renders as expected.

The Data-Retrieval Logic

Another pain point is that the ViewModelFiller attribute is tightly bound not just to a view model type, but also to the logic that provides the data. As is, this attribute is not very reusable. Having an action filter is a good idea because it keeps the code clean and readable and doesn't spoil controllers or worker services with extra behavior. However, a view model filler is inherently specific to the view that is being updated and the action that is being taken. Therefore, the filter needs to receive the logic to execute (and the view type to work on) as an argument. Here's the final format of the action filter:

[ViewModelFiller(Type=typeof(CountryViewFiller)]

public ActionResult Details(String country)

{

   ...

}

 

The now fully reusable filter receives a filler component that knows all the details of the view model—and the logic with which to fill it. The filler is a small component, aptly created to serve a particular scenario, without the faintest possibility of being reused. The point of contact between the filter and the model filler is in the following interface:

public interface IViewModelFiller

{

    void Fill(T model);

}

Each model filler class implements the interface and specifies a concrete type:

public class CountryViewFiller : IViewModelFiller

{

    public void Fill(IndexViewModel model)

    {

        model.Countries = CountryServices.GetCountries();

    }

}

You're probably going to have many such classes in a large project. They're simple, they're small, and they simply need to be plugged in where needed. Let's see what needs to be changed in the action filter.

Final Changes to the Action Filter

The action filter exposes a Type property that indicates the type of filler component to load dynamically. In the override of OnActionExecuted, the filter gets a fresh instance of the view filler component and invokes it through the IViewModelFiller interface. In doing so, the filter passes on a reference to the current view model object:

public class ViewModelFillerAttribute : ActionFilterAttribute

{

    public Type Type { get; set; } 

    public override void OnActionExecuted(ActionExecutedContext filterContext)

    {

        var model = filterContext.Controller.ViewData.Model;

        InvokeFiller(Type, model);

    }

}

You need a bit of reflection to carry things out. Figure 4 shows the implementation details of the InvokeFiller helper method mentioned previously. You use Activator.CreateInstance to create a new instance of the specified type. Next, you determine whether the type implements the expected interface (IViewModelFiller). If so, you invoke the contracted Fill method on the instance and pass the view model object to that method.

Figure 4: Reflection to invoke a view model filler

private static void InvokeFiller(Type fillerType, Object model)

{

    var instance = Activator.CreateInstance(fillerType);

    // Prepare for invoking via reflection

    var fill = GetFillMethodInfo(instance);

    if (fill == null)

        return;

    fill.Invoke(instance, new[] { model });

}

private static MethodInfo GetFillMethodInfo(Object instance)

{

    // Check that the instance implements IModelFiller and return information for method Fill

    const String methodFill = "Fill";

    var instanceType = instance.GetType();

    var interfaceName = typeof (IViewModelFiller<>).FullName;

    var interfaceType = instanceType.GetInterface(interfaceName);

    return interfaceType == null ? null : interfaceType.GetMethod(methodFill);

}

In my implementation, I added the following attribute to the filter class:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]

In this way, the attribute can be duplicated on the same method, in case I need to chain multiple calls to different fillers in the context of the same controller method. As for the AttributeTargets value, I prefer to limit applicability to the method rather than extending it to all methods in a class.

Two for One

By using the ViewModelFiller attribute, you can achieve two important design goals for your ASP.NET MVC application. First, you keep your controller (or worker service) methods extremely clean and narrowly focused. Any extra activity that the application requires (i.e., populating other parts of the view model) are accomplished by other simple and focused components, which you write as composable entities. To glue things together, use the ViewModelFiller attribute on controller methods.

As a final note, I want to spend a few words on render actions. Render actions are special controller methods that are defined to be called back from the view only, rather than publicly invoked via URL. Typically, the view calls render actions to retrieve data that it needs but that the view model doesn't provide. I'll devote an entire article to render actions in an upcoming column.

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