Sunday, 26 April 2009

ASP.NET MVC User Controls on Master Pages

NOTE: I was originally going to post the investigation into the User Control/Master Page problem AND the User Controls code in one post. However, I thought it would possibly be a bit confusing so will post separately. This explains why the lab code is called “mvc-versionedfiles”.

I am real new to MVC. Up to this point I have found it to be quite pleasurable, I really liked the separation of concerns, the conventions and the testability. Something was bound to cause a bump in the road, and I finally hit it.

The Problem

I spoke to a couple of guys (@JeremySkinner and @robinem) at WebDD09 about this problem. What I was trying to accomplish was appending the file version of my CSS/Javascript files automatically to the LINK/SCRIPT elements in the HTML within the ASP.NET Master Page. For example:

   1: <link rel="Stylesheet" type="text/css" href="site.css?1.00">

Why Bother?

The reason is this, many browsers are very aggressive with the caching of CSS and Javascript files. And perhaps rightly so, in most cases once deployed they hardly change. However, this is not ALWAYS the case, so when they do, it can be a pain in the ass since there is not a great deal we can do about it at the server side. When the page is rendered, the user will simply get the cached version of the CSS/Javascript, warts and all.

Appending the version of the file to the path of the file is a nice little hack, since the browser will interpret it as a completely different GET request, meaning it will download a new copy of the file if the version number is different. The obvious downside to this is the user will have more than one file on their machine, but at a few KB, it’s not a real problem.

So Where Does the User Control Come In?

What I wanted to do was create a User Control that would encapsulate the rendering of the appropriate tag. I know similar things can be done via Subversion and other version control systems but I wanted to avoid that since it is an application requirement, in my opinion the solution to it should be contained within the application.

How Would You Do It In WebForms?

I would simply create a User Control and chuck some code in the code-behind to do the version-checking on the file, override the Render method and jobs a “good-un” right? Sure, it may not be excellent programming practice, but it is quick and easy, which is what WebForms is all about.

What’s Different in MVC?

We want to separate our concerns. This is pretty simple, we have:

  • versionedFilesThe Model which is where the “brains” of the application is. In this context this is where we need to figure out the version of the file.
  • The View or the presentation of the model. This is obviously the HTML output of the LINK/SCRIPT tags.
  • The Controller the dude in the thick of it all that gets the data from the model and passes it to the view to get them hooked up.

Why Is That So Hard? That’s Basic MVC!

Good point, where it gets really interesting is that we want to use the User Controls in Master Pages. Master pages are really “partial views” that are mashed together with content pages at run time, so the controller does not actually get the chance to explicitly create its own “Master Page Model Data”. So the fun begins..

Attempted Solutions

Be warned, some of these were simply me trying in vain with little forethought!

Lab 01 - What the MS MVC Guys Say:" “ViewData”

My obvious first port-of-call was the ASP.NET MVC site. Where I came across this tutorial. The tutorial isn’t all bad, and covers all angles, such as what the problems are. However, the proposed solution involves passing stuff directly to the ViewData dictionary using string values as the keys. This is done within a base controller that all other controllers inherit from. While it works, I really, really have issues with using “magic strings” to access the data. They can be error prone and a pain in the ass since you lose Intellisense support. Based on this, I decided to look for alternate avenues.

Code based on this tutorial can be found in the sample code. Note the following:

  • The Master Page has to reference the ViewData items by string key.
  • The ViewData[“masterPage”] element is populated by the abstract ViewDataBaseController class.
  • The content base is strongly typed, and accesses it’s content via the Model property as normal.

Lab 02 – Master Model and Typed Master Page

Another approach I came across this on StackOverflow (thanks to @JeremySkinner). I thought this looked interesting because it appears to have all the benefits of being strongly-typed as well as allowing you to segregate the “Master Page Model”.

Check out the sample code, noting the following:

  • Our controller does not inherit from an abstract base controller (good in case we forget).
  • We have a MasterModel model that simply returns the data we are interested in as properties. This has been marked as abstract since we can never have a Master Page on it’s own, likewise we should never have the Master Model on its own.
  • The master page is now strongly typed to the MasterModel class. This allows us to simply get the data from our MasterModel with Intellisense and type support.
  • We have a basic implementation of the MasterModel for our content pages, aptly called “BasicModel” which does nothing.

Now, the problem is that we need to instantiate the BasicModel in order to spin up the base MasterModel data. If we are using the standard code in the controller to return the default view in the controller:

   1: public ActionResult Index()
   2: {
   3:     return View();
   4: }

Then we get issues because the MasterModel that is referenced in the master page has not been initialised. This makes sense because we have not told the controller to do it’s job and get the model data, so we can easily fix this by altering the code to add the BasicModel to the View’s model:

   1: return View(new MasterModelContent());

Which works, but we are back to the position of “oh crap we must remember to do this” – which I try to avoid like the plague.

Lab 03 - Master Model with Abstract Controller

OK, so the previous solution was looking pretty good, so I wanted to investigate other avenues before writing it off. The problem is that we needed to always keep repeating ourselves in our controller methods to add create the BasicModel. We want to avoid having to always pass the Model data to every View (even if the view has no Model itself) – so why don’t we create a base controller to handle this for us?

Review the sample code again, note we have added an BaseController, overriding the View method with the following code:

   1: if (model == null || !(model is MasterModel))
   2:     model = new BasicModel();
   3:  
   4: return base.View(
   5:     viewName, masterName, model);

So here we have basically encapsulated our previous problem. If there is no subclass of MasterModel passed, then create one and use that.

Straight-out of the box, this works nicely. But those on the ball will realise this is in the “Attempted Solutions” section, so where did it go wrong?

The problem came for me when I tried integrating it in to my site (specifically the contact form). When the form was being loaded, it would fall over since the contact form was strongly-typed and bound to the contact form model:

   1: [AcceptVerbs(HttpVerbs.Post)]
   2: public ActionResult Index([Bind]ContactForm form)
   3: {
   4:     return View();
   5: }

This would then fail since when we browse to the form, the BaseController will see that we do not yet have a model and then go ahead and add a new BasicModel to the model data. App go BOOM!.

formFail

Now, I totally admit here that this could be due to a complete lack of grok, knowledge/understanding of the framework on my part. But I could not work out for the life of me figure out a way in which I could get things to spin up as requested, without messing with the default behaviour in any way.

If you know a way to make this work, then please comment and let me know! I actually like this method since everything is strongly typed and would love to get over this last hurdle!

What I Went With – ViewData Wrapper Extension Methods

So at this point, I was tired, annoyed at myself for not cracking it and just wanted my bloody User Controls to work :) I decided to take a break to try and calm myself down. At this point I knew this:

  • ViewData[“nastyAssString”] worked, it was nasty, but it worked and was really quick to implement.
  • The abstract Master Model with abstract Controller worked real nice but caused issues with strongly-typed content pages. This is obviously no good since that is one of the most powerful features of MVC!

Then it hit me, why not just have both?

The problem is not the ViewData dictionary itself, it’s the way we are accessing it, so why not extend the interface so we access it in the way we desire? Lets add some extension methods to our lab (in ViewDataMasterExtensions):

   1: public static string GetHeader(
   2:  this ViewDataDictionary viewData)
   3: {
   4:     return
   5:      viewData["Master.Header"] as string;
   6: }
   7:  
   8: public static void SetHeader(
   9:  this ViewDataDictionary viewData, string header)
  10: {
  11:     viewData["Master.Header"] = header;
  12: }

Here we are simply creating some getter/setter methods for the values we are interested in, we can then set these in the BaseController:

   1: public BaseController()
   2: {
   3:     // Set the Master Page Data
   4:     ViewData.SetHeader("Header Set in 'BaseController' via Extension Method");
   5:     ViewData.SetSubheader("I can haz master page model data?");
   6: }

And also retrieve them in the master page:

   1: <h1><%
   1: = ViewData.GetHeader() 
%></h1>
   2: <h2><%
   1: = ViewData.GetSubheader() 
%></h2>

Note the Form now also works as desired. Happy face :) Just because this is simple model data, it does not mean it has to be. It could also be really complex data that is rendered by User Controls ;)

Solution Review

  • I certainly do not think this the most elegant way of doing things, but it does work and is quick to implement.
  • I was worried about using the ViewData dictionary as the hook, but this could easily be resolved by building classes to wrap sets of data and bolt them on to build a more friendly, cleaner API to the ViewData (e.g. “ViewData.Master.Header”).
  • It would be really cool if the MVC team could come up with some way for us to have a “Master Page Model” that can easily slot in and only affect the Master Page.
  • There is no hierarchy of Models (with base classes etc.), everything is hooked up in the controller, as it should be.

Just to reiterate: I am a newbie to MVC so I could be missing something! If you feel there is a better way or have pointers on what we have, then please comment!

2 comments:

  1. Thanks so much! This really helped me out. Sorry I can't help you out, as I only started using ASP.NET last week. Now if I could just figure out how to get at profile data and cookies when I don't have an HttpContext...

    ReplyDelete
  2. Great post. Particularly like the fact you did not settle for ViewData[“nastyAssString”] which is a cop out for developers.
    I have dabbled with MVC but this type of issue was a stumbling block for me to use it more.
    Fully agree with "It would be really cool if the MVC team could come up with some way for us to have a “Master Page Model” that can easily slot in and only affect the Master Page."

    ReplyDelete