Search
Categories
Navigation
Twitter Updates

Twitter Updates

    follow me on Twitter
    « 5 Things Microsoft Can Do to Encourage Open Source on Windows & .NET | Main | Demystifying the ASP.NET MVC Project Type »
    Friday
    Jul242009

    Global View Data in MVC

    ASP.NET MVC makes it easy to pass data from a controller to a view, using the ViewData dictionary or, in the case of strongly-typed views, passing the model class directly to it. There are scenarios, however, where you might want certain pieces of global data passed to your views all the time, regardless of a specific action being executed. Such scenarios can be tricky if you’re used to the Web Forms way of doing things.

    An Example Scenario

    Let’s say we want to add Google Analytics (GA) to our MVC application, and we want to store the GA tracking code in the application’s Web.config/appSettings section to make it easy to update or change across environments. Since our site only has one master page, we’ll just need to add the GA javascript code to the end of it and then get the tracking ID from appSettings.

    The Web Forms Approach

    First, let’s implement this how a lot of Web Forms developers (myself included) would initially approach it. We’ll start with a new ASP.NET MVC Web Application project.

    We’ll want to add the GA tracking ID to Web.config/appSettings, like so:

    <appSettings>
       <add key="analytics.google.trackingId" value="UA-1234567-1"/>
    </appSettings>
    

    Now, we can add the following GA code to the Site.Master page in the Views/Shared directory, right before the </body> tag:

    <script type="text/javascript">
       var gaJsHost = (("https:" == document.location.protocol) ?
          "https://ssl." : "http://www.");
       document.write(unescape("%3Cscript src='" + gaJsHost + 
          "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
    </script>
    <script type="text/javascript">
       try {
          var pageTracker = 
             _gat._getTracker('<%= WebConfigurationManager.AppSettings["analytics.google.trackingId"] %>');
          pageTracker._trackPageview();
        } catch (err) { }
    </script>
    

    We can run the application and see that the code is being pulled out of the appSettings and being injected into the page, just as we planned.

    _gat._getTracker('UA-1234567-1');

    The downside to this approach, however, is that the view is taking on too much responsibility. It knows where to get the GA tracking ID and is entirely responsible for doing so. We’ve also just created a dependency between the view and the static WebConfigurationManager class, meaning we have to deal with it when we unit test our views.

    Improving the Situation

    What we’d like to do is have the controller provide the GA tracking ID, so the view doesn’t have any insight into where it’s coming from. To do so, let’s add some code to the HomeController:

    [HandleError]
    public class HomeController : Controller
    { public ActionResult Index() { ViewData["Message"] = "Welcome to ASP.NET MVC!"; ViewData["GaTrackingId"] = WebConfigurationManager.AppSettings[ "analytics.google.trackingId"]; return View(); } public ActionResult About() { ViewData["GaTrackingId"] = WebConfigurationManager.AppSettings[ "analytics.google.trackingId"]; return View(); } }

    We’ll also update the Site.Master so that the call to WebConfigurationManager is replaced with ViewData["GaTrackingId"] like so:

    var pageTracker = _gat._getTracker('<%= ViewData["GaTrackingId"] %>');s
    

    If we run the application at this point, we should have the GA tracking ID on the / and /home/about paths, but it won’t appear on the /account/* routes as we’ve not changed that controller. This slightly improved our design as the view is once again rather stupid, unaware of where the GA tracking ID is coming from. On the other hand, we’re now violating the DRY principle as we have code duplication across the two actions (Index and About) and we’ve still got that dependency on the WebConfigurationManager class which is likely to make unit testing a pain.

    DRY it up

    MVC has an action filter system that allows you to hook into the request lifecycle, executing code before and after actions and results are executed. This is typically a good place to implement code that you may want to run for multiple actions in a controller.

    One solution would be to implement a filter that inherits from System.Web.Mvc.ActionFilterAttribute, and then annotate each of the actions or controllers that we want the filter to run for.  Alternatively, we could override the filter methods on the base Controller class itself, since these are also invoked during the request cycle. Which one is best depends on your needs, but there seems to be more examples of filter-based approaches on the web so we’ll implement an inheritance-based one here just to be different. As an aside, this probably should be implemented as an action filter as that approach would be more expressive in this instance.

    To get the GaTrackingId data into all the controllers in the application, we’ll add a new abstract controller class that will then serve as a base controller for our other controllers. Here’s the code for the ApplicationController:

    public abstract class ApplicationController : Controller
    {
        protected override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            ViewData["GaTrackingId"] = 
                WebConfigurationManager.AppSettings[
                "analytics.google.trackingId"];
    
            base.OnActionExecuted(filterContext);
        }
    }
    

    Now we’ll make the HomeController and AccountController classes inherit from ApplicationController, instead of Controller and remove the lines where we were adding the GaTrackingId value in the HomeController actions, since this is now being handled in the base class.

    At this point, we should have our GA tracking ID available globally within the application, and the views are all stupid, without any knowledge of how this is occurring.

    Further Improvements

    This is a better design than what we started with, but we still have that dependency on WebConfigurationManager that will make unit testing our controllers (all of them now) more complicated than it should be. Additionally, the master page may not be the best place for the GA code in the first place.

    I’ll leave this improvement for my next post, however, since it will introduce dependency injection using Ninject.

    Is This Really Better?

    A pragmatic developer may look at this code and ask whether or not we’re actually improving it. After all, the initial Web Forms way of implementing this was a single line of bee-sting notation, but to implement it using a more MVC-based approach we added a new base controller class, altered the inheritance chain for our controllers and left that bee-sting line in the view (albeit changing it slightly to read from ViewData).

    A common issue for MVC, and many other design patterns, is that their value isn’t readily apparent in simple examples, and really only come to light when a system’s complexity scales up. Just as with Web Forms, the danger of putting all the application’s logic into the view isn’t obvious when a system is first built; the requirements are typically known and we’re executing on them. In six months, however, things change and we may now want to alter how the application behaves. By embedding too much logic into the view, it’s more difficult to implement such change without affecting other things.

    What happens, for instance, if we want to move to an alternative view engine that doesn’t support in-line code at all? Alternatively, what if we decide to begin dynamically selecting the GA Tracking ID based on the sub-domain of the request?

    Admittedly, the solution outlined above is more complex than our initial implementation, but now changes in the tracking ID retrieval logic absolutely won’t affect the view and vice versa. While such a simple example doesn’t fully illustrate the benefits of the pattern, real world applications typically have a few dozen additional dependencies. These dependency chains can quickly make even the simplest of changes take much longer than expected, which is why the decoupled, separation of concerns approach that MVC espouses is so powerful.

    This post was written using Visual Studio 2008, ASP.NET MVC v1.0 and .NET 3.5. The source of the demo application is available for download.

    PrintView Printer Friendly Version

    EmailEmail Article to Friend

    Reader Comments (1)

    Useful! Thanks

    May 19, 2010 | Unregistered CommenterBen
    Comments for this entry have been disabled. Additional comments may not be added to this entry at this time.