Referencing stylesheets, scripts from content pages

The ability to reference style sheets and scripts from within content pages is greatly needed, but link and meta tags only work in the root master page. Script references don't work anywhere... that's a separate issue. Here's how to add full link, meta, and script parsing to your entire application. Because the parsing logic for 'link', 'meta', and 'title' is only inside the HtmlHead class (in the HtmlHeadBuilder class), link and meta tags don't work in content pages. If you put a <asp:ContentPlaceHolder> tag in the head section of the master page, and a matching <asp:Content> tag in the content page, you've created a barrier the special parsing logic can't cross. I wrote a series of tests to determine exactly when <script>, <link>, and <meta> parsing stop working. It's kind of like an 'acid test' for this issue. It also includes 8 tests for a 2nd bug with ContentPlaceHolder. While 2 of those always fail, the rest turn green when you hit 'Enable patch". Only Microsoft can fix the last two, since the methods are marked 'internal'. If you haven't tried using application-relative paths, you might not have noticed the issue yet. Relative paths will appear to work if the physical path of the page matches the browser path, i.e, no nested master pages, no URL rewriting or Server.Transfer. Root-relative paths will work, but using them is a short-sighted mistake.. So, what does happen to them? Well, if your link and meta tags have runat="server" on them, then the generic parsing code will turn them into HtmlGenericControl instances. If not, the tags are left as plain text and packed together in a LiteralControl instance between other server tags. HtmlGenericControl doesn't do path resolution, so adding runat="server" doesn't help any.

My solution

I wrote an algorithm that does a 2nd parsing pass to make everything act like it should. While I was at it, I also added support for script references in the head section. The algorithm
  1. Parses the leftover LiteralControl instances in Page.Header and turns <link>, <meta>, and <script> tags into HtmlMeta, HtmlLink, and ScriptReference(custom class) instances. The remnants of the LiteralControl are split into multiple LiteralControl instances. tags inside client or server comments are ignored.
  2. Recursively loops through all HtmlGeneric control instances in Page.Header and turns <link>, <meta>, and <script> tags into HtmlMeta, HtmlLink or ScriptReference instances (This catches the ones that twere parsed by the generic parser because they had runat="server" specified.)
When dynamically creating controls directly inside a ContentPlaceHolder, it's critical that you assign the TemplateControl property correctly if you want URL rebasing to work. If you don't, the control will ask it's parent (the CPH), what the TemplateControl is, and the CPH will return the master page. Then everything will get rebased relative to the master page instead. In my algorithm, I use the GetAdjustedParentTemplateControl() method to adjust for the CPH issue when setting the TemplateControl property.

What if it's fixed?

The patch just looks at the leftovers. If Microsoft fixes this behavior, there won't be any leftovers.

What about performance?

Regular expressions are very, very fast, and we're only running them on leftover LiteralControls. On a large head section, timing tests hovered around 0.7 milliseconds. I've been using this on a high-traffic site for over a year.

The code

The two main files are ContentPlaceHolderFixes.cs and ScriptReference.cs. To repair the head section on the current page, call ContentPlaceHolderFixes.RepairPageHeader(this); during OnLoad. I recommend sub-classing Page and doing this application-wide. You'll end up wanting to do this at some time or another anyway.
    public class PageBase : Page
    {
        /// <summary>
        /// Creates a new PageBase instance.
        /// </summary>
        public PageBase(){}

        /// <summary>
        /// Calls the ContentPlaceHolderHeadRepair.Repair method
        /// </summary>
        /// <param name="e"></param>
        protected override void OnLoad(EventArgs e)
        {
            //Parses link, meta, and script tags that got missed by the Head>CPH bug.
            ContentPlaceHolderFixes.RepairPageHeader(this);

            //Fire the events later
            base.OnLoad(e);
        }

    }
To use this base class, tell your code-behind file to inherit from PageBase instead of Page. If you don't have a code-behind, set Inherits="PageBase" on the page directive.

Tests

The project includes a "Test" folder and a Test.aspx file. Run it to compare results with and without the patch. It contains about 26 tests, which exercise the patch under different conditions: nested master pages, runat="server", visible="false". etc. Test.aspx uses Server.Transfer to simulate URL rewriting. I'll be very happy when those 26 tests succeed without needing the patch, and I'm sure everyone else will also. If you're interested in monitoring the progress of the issue, you can check out the Micrsoft Connect ticket. Get WebFormFixes Run live test

Alternative solutions and their issues

Here are some alternative solutions I've tried and read about. I wasn't really satisfied with any of them.
  1. Create an HtmlLink instance, assign the properties, and add it to Page.Header from a code-behind file. Adding a code-behind file isn't something designers typically feel comfortable with, and it adds clutter.
  2. Use inline code to resolve the URL: <link type="text/css" rel="stylesheet" href='<%= ResolveUrl("styles.css") %>' />. Check out this article about why code blocks inside master pages can cause problems. I ran into the issue myself, and it's frustrating.
  3. Create a custom control for embedding stylesheets to replace the 'link' tag. This sounds the best of the alternatives, assuming you reference the control globally in Web.config. I haven't tried this one, though, so YMMV.
These alternatives all require markup changes, and will prevent WYSIWYG editors from reading the stylesheet references properly. I'm adverse to making things any more non-standard than they have to be. I like to implement a patch such that I can forget it exists.

See Also

A more comprehensive subclass that allows runtime management of stylesheets and metadata.

Published on

About Nathanael

Nathanael Jones is a software engineer, husband, consultant, and computer linguist with unreasonably high expectations of inanimate objects. He refines .NET, ruby, and javascript libraries full-time at Imazen, but can often be found on stack overflow or participating in W3C community groups.

ImageResizer

If you develop websites, and those websites have images, ImageResizer can make your life much eaiser. Find out more at imageresizing.net.

Recent Tweets

| Loading recent tweets...

Imazen

I run Imazen, a tiny software company that specializes in web-based image processing and other difficult engineering problems. I spend most of my time writing image-processing code in C#, web apps in Ruby, and documentation in Markdown. Check out some of my current projects.