Windows Phone 7 Developer Launch - Learn More
kick it on DotNetKicks.com   Shout it  

ASP.NET MVC: Discover the MasterPageFile Value at Runtime

A couple weeks ago it was finally time to add a context-sensitive, data driven menu system to our MVC application. As I thought about it I was stuck. I wasn't sure what the best way to implement it was. As is common with an MVC application there was no 1-to-1 relationship between actions and views. And even more difficult was that our *.master files could be used by views tied to different controllers. So it was looking like I would have to load the data I needed from the ViewMasterPage.

I really didn't like this option and looked around a bit trying to find out what others had done. Here's a couple examples of what I found:

While all of these options work, none of them sat well with me because they either require me to remember to include the data or they feel contrived or foreign to MVC.

@Page Directive

When you create a new View file you can specify that you want to use a MasterPage. When you do this your @Page Directive will look like this:

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Default.Master" Inherits="System.Web.MVC.ViewPage" %>

This can be changed as needed but if you are using MasterPages in your application you the value of the MasterPageFile is exactly what you need to determine which MasterPage is being used by the view being returned. I like this idea because the same action can return different views, or even result in a redirect, so it isn't until you actually arrive at the controller's ActionExecuted event that you know for sure that the result is a View and which view that will be.

Controller.OnActionExecuted event

The key to the whole thing is you need to be able to read the @Page directive located in the first line of your ViewPage. When you're handling the OnActionExecuted event you get an ActionExecutedContext object passed in from System.Web.MVC.ControllerBase which contains the result of Action which just finished executing. Here's what you do to get from the start of the event to the value of MasterPageFile:

  1. Check to see if ActionExecutedContext.Result is an ViewResult
  2. Check to see if ViewResult.ViewName has been set (if you're writing tests for your Actions you'll be doing this anyway). If it hasn't then you know that the name of your view will be the same as the Action, so you can get the value from ControllerContext.RouteData.
  3. As long as you are using the WebForms view engine (or inheriting from it) you can use the ViewResult.ViewEngineCollection.FindView method to let the ViewEngine find the view for you.
  4. FindView returns a ViewEngineResult has a View property which returns a WebFormView which in turn has a ViewPath property.
  5. At this point you can get the source of your view, parse it and retrieve the value of MasterPageFile. Once you've done this I'd recommend caching the value to prevent the need to parse the file every time.

Here's what the full implementation looks like:

using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
 
namespace GPM.Web.Code {
    public class MasterMenuDataFilterAttribute : ActionFilterAttribute {
 
        // private vairables supporting MasterPageFile discovery
        private static Dictionary<string, string> _viewMasterType = new Dictionary<string, string>();
        private static Regex _masterFilePath = new Regex("\\bMasterPageFile=\"(?<path>[^\"]*)\"", RegexOptions.Compiled);
 
        // private members for improved readability
        private HttpContextBase _httpContext;
        private ControllerContext _controllerContext;
 
        /// <summary>
        /// Loads data for dynamic menus in our MasterPage (if applicable)
        /// </summary>
        private void LoadMenuData(string viewName, string masterPath) {
            if (string.IsNullOrEmpty(masterPath) || !System.IO.File.Exists(_httpContext.Server.MapPath(masterPath)))
                return;
 
            switch (Path.GetFileName(masterPath)) {
                case "Site.Master":
                    break;
                case "Default.Master":
                    break;
                case "Custom.Master":
                    break;
                default:
                    break;
            }
        }
 
        /// <summary>
        /// Discovers the master page declared by the view so we can determine
        /// which menu data we need loaded for the view
        /// </summary>
        /// <remarks>
        /// If we find that we have too many controllers which don't need this 
        /// functionality we can impelment this as a filter attribute instead
        /// and apply it only where needed.
        /// </remarks>
        public override void OnActionExecuted(ActionExecutedContext filterContext) {
            // this logic only applies to ViewResult 
            ViewResult result = filterContext.Result as ViewResult;
            if (result == null)
                return;
 
            // store contexts as private members to make things easier
            _httpContext = filterContext.HttpContext;
            _controllerContext = filterContext.Controller.ControllerContext;
 
            // get the default value for ViewName
            if (string.IsNullOrEmpty(result.ViewName))
                result.ViewName = _controllerContext.RouteData.GetRequiredString("action");
 
            string cacheKey = _controllerContext.Controller.ToString() + "_" + result.ViewName;
            // check to see if we have cached the MasterPageFile for this view
            if (_viewMasterType.ContainsKey(cacheKey)) {
                // Load the data for the menus in our MasterPage
                LoadMenuData(result.ViewName, _viewMasterType[cacheKey]);
                return;
            }
 
            // get the MasterPageFile (if any)
            string masterPath = DiscoverMasterPath(result);
 
            // make sure this is thread-safe
            lock (_viewMasterType) {
                // cache the value of MasterPageFile
                if (!_viewMasterType.ContainsKey(cacheKey)) {
                    _viewMasterType.Add(cacheKey, masterPath);
                }
            }
 
            // now we can load the data for the menus in our MasterPage
            LoadMenuData(result.ViewName, masterPath);
        }
 
        /// <summary>
        /// Parses the View's source for the MasterPageFile attribute of the Page directive
        /// </summary>
        /// <param name="result">The ViewResult returned from the Controller's action</param>
        /// <returns>The value of the Page directive's MasterPageFile attribute</returns>
        private string DiscoverMasterPath(ViewResult result) {
            string masterPath = string.Empty;
 
            // get the view
            ViewEngineResult engineResult = result.ViewEngineCollection.FindView(
                _controllerContext, result.ViewName, result.MasterName);
 
            // oops! caller is going to throw a "view not found" exception for us, so just exit now
            if (engineResult.View == null)
                return string.Empty;
 
            // we currently only support the WebForms view engine, so we'll exit if it isn't WebFormView
            WebFormView view = engineResult.View as WebFormView;
            if (view == null)
                return string.Empty;
 
            // open file contents and read header for MasterPage directive
            using (StreamReader reader = System.IO.File.OpenText(_httpContext.Server.MapPath(view.ViewPath))) {
                // flag to help short circuit our loop early
                bool readingDirective = false;
                while (!reader.EndOfStream) {
                    string line = reader.ReadLine();
 
                    // don't bother with empty lines
                    if (string.IsNullOrEmpty(line))
                        continue;
 
                    // check to see if the current line contains the Page directive
                    if (line.IndexOf("<%@ Page") != -1)
                        readingDirective = true;
 
                    // if we're reading the Page directive, check this line for the MasterPageFile attribute
                    if (readingDirective) {
                        Match filePath = _masterFilePath.Match(line);
                        if (filePath.Success) {
                            // found it - exit loop
                            masterPath = filePath.Groups["path"].Value;
                            break;
                        }
                    }
 
                    // check to see if we're done reading the page directive (multiline directive)
                    if (readingDirective && line.IndexOf("%>") != -1)
                        break;  // no MasterPageFile attribute found
                }
            }
 
            return masterPath;
        }
    }
}

I've implemented this as an ActionFilterAttribute so you can just apply it to any controller or action. This way you can use it in a more flexible way. The only thing left for you to do is fill in the blanks in the LoadData method to retrieve the data you need based on the name of the MasterPageFile.

Conclusion

We've been running this setup for a couple weeks now in development, QA and UA and it's working like a charm so far. Once you have it setup, you're free to forget about it until you need to change how your menus function or your data set. Plus now you're keeping all your interactions with your model inside your controller and your view just needs to pull the data from the ViewDataDictionary.

Tags:

kick it on DotNetKicks.com   Shout it  

Feedback

# 

Gravatar It is late and I am tired and too lazy to think this through at the moment, but what if you are dynamically setting the master page that a view is using? Will this still work? From the sounds of it, at least, the way you wrote this, it sounds as if you are literally going to get the value that was in the page directive, which will not work if I dynamically change it at runtime.

Anyway, thanks for the info, I am going to re-read this tomorrow just because I want to read it again when I am more lucid. :)

Also, I am curious as to your opinion on what I am doing that causes me to ask this question - I have a website I am building, for fun, that uses different master pages to dictate the "theme," if you will, of the site. Since the chosen theme can vary based on the user's preferences (or some other arbitrary setting), I need to dynamically set the master page used by the views. Currently, I am doing this in a class that inherits from ViewPage and serves as the base class for all of my views. I arrived at this decision, for good or bad, because I figured the view is responsible - but now, I am not so sure! Should the controller decide that? The controller *does* decide which view to show in most cases, but should that include what the view looks like?!

My reasoning behind my choice is based on the thought that the controller picks a view based on the *behavior* of the view, i.e. the intent of that view's existence - the fact that the view has a specific look and feel is always dependent upon code (CSS, HTML, JavaScript, etc.) *in* the view anyway, outside of the purview of the controller, and thus is secondary to the reason the controller chose that view anyway.

Right?

Ah, I wish things were cut and dry on this, but I still don't feel 100% sure - I am still leaning towards letting the view decide what the user sees as far as look and feel, mainly based on the rhetoric I just elucidated. What say ye, Mark? I know this is a bit OT, but it is the reason I asked my original, relevant question. :) 7/26/2009 9:32 PM | noreply@blogger.com (JasonBunting)

# 

Gravatar By the way, I just read my comment after I posted it and feel even more strongly that I am "right" in feeling the way I do about *where* in the MVC pattern the decision to use a given master page is made.

But I want to hear someone else's analysis, so let me have it. Support me or tear me down, just do it and back it up! 7/26/2009 9:37 PM | noreply@blogger.com (JasonBunting)

# 

Gravatar To answer your first question, my code sample doesn't handle the case where the Action explicitly sets the master page. However, adding support for that scenario is trivial.

In the OnActionExecuted method, after you verify the result is of type ViewResult you can check the value of the result's "MasterName" property. If the value is not null or empty then you can use that value and forward it to the LoadMenu data method - just make sure you account for the fact that it will come thru without the .Master suffix.

On your second question, I don't feel real strongly about this one way or the other wither the decision is made by the controller or the view. However, I do feel strongly about the view retrieving data or connecting to services. If you want to pass the data to the view from the controller via ViewData, then make your decision based on the view then that works for me. In fact that's probably how the above mentioned "MasterPage" was intended to be used. Since the name doesn't have to refer to a specific view it can be a key which means something to your view engine. It's completely agnostic of your view engine implementation.

If you need more than just a string name to make your view decision, then it's up to you to add it to the view model. But either way, you should be done communicating with your model repository or any services by the time your controller passes control over to your view engine. The reasoning behind this is that the controller is easier to test, logic in the view should be minimal - not so complex that the functionality can't be verified by simple manual testing. 7/27/2009 7:55 AM | noreply@blogger.com (Mark J. Miller)

# 

Gravatar I don't recall saying anything about the Action setting the Master Page...did I? Maybe I did, via implication, but I didn't mean it. I would never allow an action to set a master page, that doesn't make sense to me.

As for what the view should and should not do, I think that as long as any logic a view has within it is strictly germane to the look and feel of a view, not the data or logic of the view, per se, then I am okay with it.

A view is a view, and the minute my controller is passing look and feel information to a view, other than the data that the view will render related to the domain model, then that's probably outside the scope of what should happen - right? I am trying to think of a case where a view connecting to a service would be valid. I don't know that I would care if a view wants to connect to a service to get data related to colors or styles it is using. I mean, honestly - why should I have a controller passing around CSS information? That doesn't make sense to me.

I don't know, but I appreciate your opinion. I am still trying to formulate my own feelings on this pattern and the pros and cons of the approaches people use. 7/27/2009 11:43 AM | noreply@blogger.com (JasonBunting)

# 

Gravatar JasonBunting >> "I don't recall saying anything about the Action setting the Master Page...did I?"

You said:

JasonBunting >> "what if you are dynamically setting the master page that a view is using?"

I just assumed you meant the "MasterName" parameter of the basecontroller's overloaded View(string, string) method. As to your argument against it, I agree that if you are using the default ViewEngine I would never use it for the same reason you stated. I don't believe the controller should be explicitly telling the view which master page to use.

However, in your example where the master is dynamically selected based on user preferences I think it is appropriate. I don't think hard-coding the master in the action is appropriate, but when it is coming from user preferences the selected master page is dynamic. Plus, the "MasterName" is not a path, just a name (key). If you are passing a file path for the master from the action to the view engine this would be very wrong indeed. But if we're talking about a user's preferred skin and we'll just say the value is "green" indicating the user's preference is for the theme named "green" I don't see what's wrong with it.

I admit that if you are storing HTML and CSS in the database things become a little grey and I'll have to give that more thought. But I wasn't referring to that anyway. For the moment I'm referring only to predefined, existing master pages which could define separate themes. In the example I mentioned above, the controller isn't setting the master, it is just forwarding the user's selected value to the view engine. The master itself would be defined to reference the correct CSS files to provide the theme and so the controller knows nothing about the view or the master - at least no more than it does by selecting the view to be displayed (ie. Controller.View(string) 7/27/2009 12:07 PM | noreply@blogger.com (Mark J. Miller)

# 

Gravatar I was in a bit of a hurry when I wrote that last comment and I wanted to amend it a little. I said that I wouldn't use the MasterName parameter with the default ViewEngine. If the theme choices were simple and there was a one-to-one mapping between the user's choices and master pages then I don't see any reason not to use the MasterName with the default ViewEngine. If my themes were named "Red", "Blue" and "Green" then I would just have 3 master pages with the same names and they would map directly.

However, I would not actually call the View(string, string) method from my action. I will have to consent to your argument that it doesn't make sense for the action to be responsible for that. But the controller still can be. Which means I would override the implementation of the controller's View() method and that is where it makes the most sense to determine (from user preferences) to assign the value for MasterName.

This way the action doesn't know/care, the view doesn't interact with any services (user preferences/profile), it follows the DRY principle and it is simple. 7/27/2009 2:02 PM | noreply@blogger.com (Mark J. Miller)

# 

Gravatar Yeah, you shouldn't assume anything because you make an...

You know what I mean. No, I have been doing the setting of the master page by overriding the OnPreInit method of the ViewPage within my own BaseViewPage class. I had not yet gotten to *how* the BaseViewPage class was going to obtain the information on which master to use, because for the time being I merely hardcoded a master page different from the one in the page directives in the views (since there is a default, and then users can choose others - at least, that is the idea).

So, after reading your comments and thinking about this more, I think you identified the better way of doing this, that being to handle it in a controller base class instead. At least, that sounds better to me at the moment. Give it another 12 hours and I may change my mind again. :)

I am actually going to sit on that part of my app for another day or two since I really want to think about it more; that, and I have other things I can do that are a more valuable use of my time for now.

And I don't know that I would *ever* store CSS in a database unless that was really called for and it was dead obvious to do it. I would avoid that at all costs, and that wasn't at all what I had in mind when I made that comment from which you inferred I implied that. :)

As for the overloaded View(string, string) method: yikes! I definitely think that overload should be done away with, I don't understand who would think an action should decide which master page a view should use, but perhaps I am missing something obvious other than the fact that they may have simply done it to make people in the community happy. I don't know how it was decided, but it doesn't seem right. 7/27/2009 2:58 PM | noreply@blogger.com (JasonBunting)

# 

Gravatar Well, I ended up doing the deed and I am pretty happy with this implementation for the time being.

Basically, I just overrode the only two View() methods that matter (because all of the others on the Controller class call either of these two):

View(IView view, object model)
View(string viewName, string masterName, object model)

If you are interested in seeing what it looks like, you can see the basics of the implementation of the class for the next month. 7/27/2009 9:38 PM | noreply@blogger.com (JasonBunting)

# 

Gravatar Nice, I checked out your implementation and I think that's the right choice. 7/28/2009 9:03 AM | noreply@blogger.com (Mark J. Miller)

# 

Gravatar Thanks for verifying the validity of my implementation - as always, I am forever in debt to your priceless advice.

:P 7/28/2009 9:47 PM | noreply@blogger.com (JasonBunting)

# lululemon headbands

Gravatar ASP.NET MVC: Discover the MasterPageFile Value at Runtime 10/2/2014 7:07 AM | lululemon headbands

Comments have been closed on this topic.
 

 

Copyright © Mark J. Miller