The ContentPlaceHolder – Not Just For Content

Tuesday, April 11, 2006

I’m working on an article full of tips and tricks for master pages. The article will be online soon, but I wanted to throw out one tip as a preview and get some feedback.  

A master page typically includes the single HTML <head> tag for a page. This approach gives every content page a <head>, but presents problems when a content page wants to add additional tags inside head, like <meta> and <link>, for example. The tags might be specific to the content page, and we wouldn’t want to include them in the .master for every page.

As long as the <head> tag has a runat=”server” attribute defined, a content page can programmatically add to <head>. The recommended ASP.NET approach to add a <link> tag, for example, is to use the following code:

HtmlLink link = new HtmlLink();
link.Href =
"~/wildStyle.css";
link.Attributes.Add(
"type", "text/css");
link.Attributes.Add(
"rel", "stylesheet");
this.Header.Controls.Add(link);

For more Header examples, see Sue’s edreams.org post.

Milan thinks the Header API is lacking functionality, and I’d agree. My frustration level rises whenever I have to modify the <head> tag programmatically, so for kicks, I tried putting a ContentPlaceHolder inside of the <head> tag:

<%@ Master Language="C#" %>
...
<head runat="server">
    ...
    <asp:ContentPlaceHolder runat="server" id="headerPlaceHolder" />
</
head>
...

Then in a web form I used a Content control to add additional scripts and metadata inside of <head>:

<%@ Page Language="C#"
        
MasterPageFile="~/Master1.master"
        
Title="Foo" %>

<asp:Content ID="Content1"
            
ContentPlaceHolderID="headerPlaceHolder"
            
runat="Server">
             
  <meta name="keywords" content="declassified information" />
  <style type="text/css" media="all">
      @import "wildstyle.css";
  
</style>
  <script type="text/javascript" src="foo.js"></script>
  
</asp:Content>

Lo and behold, this works and feels natural. No more Header.Controls.BlecktyBlah!

Except, there is a catch....

The validation engine in the Visual Studio 2005 IDE doesn’t believe a ContentPlaceHolder control belongs inside of <head>, so it raises a validation error. This is just a validation error, so the application still compiles, and still executes, but it’s an irritating validation error nonetheless, and one we can’t get rid of unless we disable all HTML validation. Ugh.

Update: The rest of this post was a late night ramble full of rubbish that didn't produce the intended result.

Move along now, nothing to see here.

To appease the deities of validation, I can resort to a hack. I can move the master's ContentPlaceHolder for <head> related goo inside of the <form> tag. This avoids validation error flags, and feels justifiable because all the webforms look more maintainable. The master page only needs to swap the ContentPlaceHolder into the Header controls collection during the page lifecycle.

In other words, with a master page like this:

<%@ Master Language="C#" %>

...
<head runat
="server">
    <title>Untitled Page</title>
    
</head
>
...
    <form id="form1" runat
="server">
     <asp:ContentPlaceHolder runat="server" id="headerPlaceHolder"
/>
     ...
    </form
>
...

All we need is some code behind the master page, or in a master page base class like so:

protected void Page_Init(object sender, EventArgs e)
{
  Controls.Remove(headerPlaceHolder);
  Page.Header.Controls.Add(headerPlaceHolder);
}

The .aspx webforms can add to <head> declaratively, using a Content control.

What do you think? Good? Bad? Dirty?


Comments
scottgu@microsoft.com Tuesday, April 11, 2006
Magic URL rebasing is actually built-into the header control, which means you actually don't need to use a ContentPlaceholder control or write code to declare a relative stylesheet reference above.

Just write:

<link href="StyleSheet.css" rel="stylesheet" type="text/css" />

within the head section of your .master page.

The head server control will then automaticlaly update the stylsheet path to always be relative to the master page location regardless of what sub-directory a page based on the master page is. For example, if you put a page1.aspx page within a "NewFolder" subdirectory in your site, the control would automaticlaly adjust the reference to the stylesheet to be "../Stylesheet.css" for you. If you had a page2.aspx in the same directory as Site.Master, then it would be declared as just "StyleSheet.css".

This saves you from having to write any code at all.

Hope this helps,

Scott
Anatoly Lubarsky Tuesday, April 11, 2006
I think it is bad and dirty. Remove and add the same control to another place does not seem like natural flow.

For me the recommended ASP.NET approach option(adding optional tags programmatically) looks the best option for today, although it has disadvantages such as you mentioned and also others such as bad rendering for example.

martinkr Tuesday, April 11, 2006
Sorry, I can not evaluate your code. I am a cut and past programmer (kayaking is my work). But I have tryed to use Atlas with masterpages and mousover on my homepage. I get "no associatet element for the hyperLink" and think this has to do with the "contenspage" problem in postback. Will this given code help me or have you some line of code that solve my problem ?
scott Tuesday, April 11, 2006
ScottGu: I'm talking about optional tags you want to add from a content page because you don't want them in the header.

MartinKr: Sorry, I'm not sure what can cause that problem.

Anatoly: thanks for the feedback :) I hope the next version of asp.net can provide a cleaner way to do this (but that too far in the future to even dream about at this point).
Steve Tuesday, April 11, 2006
Interesting. This, too is the approach that I've settled on (without the additional step of stuffing the header content inside of the form tag). As to the validation errors, I just live with it and hope that it will change in the next go around.

Also, as a somewhat tangential aside, this is also part of my client-side script management strategy since ASP.NET insists on placing all of the RegisterClientBLAHBLAH stuff in the BODY and not the HEAD. I've seen posts back and forth why this shouldn't matter, but this is not a compromise I'm willing to make so I manage it all myself.

Thanks.
scott Tuesday, April 11, 2006
As it turns out, Steve. The validation engine caught up to me again and now complains that "meta" can't live inside a form tag. Grrr. Not sure why it didn't catch this earlier, I didn't think it could figure out where the Content controls actually gets placed in the master, but maybe its just guessing.

In any case then, forget the hack. I guess we have to live with the validation error and use a placeholder outside of the form.
scottgu@microsoft.com Wednesday, April 12, 2006
Note that the validation schemas are just XML files -- so if you want you could always customize/tweak them however you want to support this. Alternatively, you can also just turn HTML validation off.

This blog post covers both topics more: weblogs.asp.net/.../437228.aspx

Hope this helps,

Scott
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!
(c) OdeToCode LLC 2004 - 2014