How to Link Stylesheets from a Master Page

Posted on April 09, 2006  |  

Posted in Development

31 comments

First off, big thanks to Scott Allen for his coverage of master pages in ASP.NET 2.0. Scott nicely summarized some common gotchas that would’ve cost me time if I wasn’t aware of them.

One of the dilemmas master pages present is how to link to images, web pages, stylesheets, etc. Do you use a relative URL? Or an absolute one? Do you give, say, an image a runat="server" attribute and throw a tilde (~) in to have it resolved to the project root? There’s no easy answer.

HtmlHead Is Lacking

In 2.0 you may access the <head> element of a page server-side (provided you decorated it with runat="server"). The runtime treats it as an instance of the HtmlHead control.

What jumped out at me was its scarce list of useful methods and properties. Yes, there’s the much coveted Title attribute, but besides that… How do I register an external JavaScript file? Or a stylesheet? Or a meta tag. Granted, meta tags are for the most part only “hints” to browsers and therefore aren’t that useful, but still.

For example, I like to @import some of the stylesheets, and <link> the rest:

<style type='text/css' media='screen,projection'>
@import '/styles/layout.css';
</style>
<link rel='stylesheet' type='text/css' 
     media='print' href='/styles/print.css' />

You might want to do this to cater advanced styles to modern browsers, and older styles to legacy browsers. This way every browser gets what it can handle. Or, this way you may let owners of crappy browsers know it’s time to upgrade.

The HtmlHead control doesn’t have the luxury of doing much besides setting the page title and registering page-scope CSS rules. It’s a shame.

Some hacks, published online, suggest creating an HtmlLink control, setting its src and rel properties, adding it to the Controls collection, or resorting to even funkier logic with AddParsedSubObject(). All this to register a stylesheet and only a <link> one?! Maybe we should learn a thing or two from the Ruby on Rails folks. At least Rails was written by developers for developers.

My Hack

I wrote a base class for for my master pages. It simply exposes a property with the base URL of my project. If your project can switch between HTTP and HTTPS, you might want to add another private property for that instead of hardcoding “http://”.

When it comes to creating a new master page, I derive from my base class and plug the BaseURL property as follows:

<style type='text/css' media='screen,projection'>
@import '<%= BaseURL %>/styles/layout.css';
</style>
<link rel='stylesheet' type='text/css' 
    media='print' href='<%= BaseURL %>/styles/print.css' />

A master page is a good old combination of HTML markup and code-behind, which is why you can reference its properties.

31 comments

scottgu
on April 9, 2006

Sue has a good list of samples showing common head usage here: http://www.edream.org/BlogArticle.aspx?RecordID=112

Hope this helps,

Scott


Tim Murphy
on April 10, 2006

You might want to check out this post on my old blog as an alternate approach.

http://twmurph.blogspot.com/2006/01/master-pages-and-themes.html


Milan Negovan
on April 10, 2006

Scott, that's my point exactly: "Page.Header.Controls.Add" is an unnatural notation. For example, which line of code is more natural?

if (string.Contains ("something")) {}
or
if (string.IndexOf ("something") != -1) {}

The first one, of course. The HtmlHead control makes you think in terms of control trees and such. That's not how you think when you lay out web forms. That's why I said HtmlHead was lacking meaningful functionality.


Karl
on April 10, 2006

This is a problem I have been trying to solve for a long time. Thanks for writing about this.


Scott
on April 10, 2006

Thanks for the compliment. I actually started working on another master page article this weekend that covers a lot of tips, tricks, and tracks. One thing I've discovered is that ContentPlaceHolders work well inside the tag (unfortunately, VS2005 validation flags the control with an error, but everything works). In any case, this article keeps growing and should provide a couple tips on stuff.


scottgu
on April 10, 2006

Ahh...but you forget magic URL rebasing is built-into the header control, which means you don't need the BaseUrl property above for the link element at all (or any code for that matter).

Just write:



within the head section of your .master page.

The < head > server control will then update the 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.... ;-)

Scott


scottgu
on April 10, 2006

Rats - my comment above was html encoded. Put the appropriate html brackets around this to have it show up:

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


Milan Negovan
on April 11, 2006

Aha! "Relative to the master page location" was the missing bit. Thanks, Scott!


Dave Griffiths
on April 13, 2006

Why not use ResolveUrl() and get the best of both worlds (late/dynamic resolution, without a server control)?

@import '< %= ResolveUrl("~/styles/layout.css") % >'

ResolveUrl is a method on all controls and pages, and has 2 flavours, whether you use Me.Page.ResolveUrl or Me.ResolveUrl (or simply ResolveUrl) determines where the resolution is relative to (i.e. in both cases it's relative to the relevant controls Template source folder), so Me.ResolveUrl() is relative to the control or page ASPX/ASCX folder, and for Me.Page.ResolveUrl() is relative to the containing ASPX pages folder. It's useful for embedding in ASCX's that need to access a resource relative to the location of the containing page. Using the tilde (or an absolute path) makes both versions act the same ...


Taco Oosterkamp
on April 15, 2006

Milan, thanks for this article. It helped me fix a problem with including a reference to a JavaScript file. I now use a combination of your BaseURL code and the code of Sue that Scott pointed to.

Scott, you gave an example of linking to a css file. That was no problem for me using themes, but do you know of a way to do the same thing for a JavaScript file. It looks quite a bit cleaner than having to write C# code, but I need an example to get this working, I'm afraid.

Thanks!
Taco Oosterkamp


Milan Negovan
on April 17, 2006

Dave, that's a great idea! It's a pity that classes and methods which process URIs are scattered all over .NET.


franco
on September 13, 2006

can you suggest validating web controls vs2003 using external javascript validation file.i have no clue and i am goin to use it for the first time


Milan Negovan
on September 14, 2006

Franco, I honestly don't know what external validation file you mean.


franco
on September 14, 2006

hi,
i want to validate for eg email,url entered by the person in web control on mine form.this validations are required on multiple pages.so i thought of using a single javascript file .


bobC
on January 23, 2007

I I have a root site with child sites. Each child site is based on a master. Each child site needs a unique logo, but when the image tag is resolved, it is always based on the location of the master page, at the root, so images always come from the root instead of the sub-site images folder.

How can I use a master page for sub-sites, and pull images from the sub-site Images folder?

RootSite
Images
SubSite
Images

SubSite
Images


groker
on November 9, 2007

Another method:
The advantage of this method is that you set the master page up as a you would any html page and thus see the styles in the designer.


just add the following to the master page or master page base:

protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
JSManager.MaintainScrollPosition(Page);
if (!IsPostBack)
{
foreach (Control control in Page.Header.Controls)
{
WebControl webControl = control as WebControl;
if (webControl != null && webControl.Attributes["href"] != null)
webControl.Attributes["href"] = ResolveUrl(webControl.Attributes["href"]);
}
}
}


groker
on November 9, 2007

sorry, take the line "JSManager.MaintainScrollPosition(Page);" from the above example


Mike Flynn
on April 19, 2008

This is a good example but it does not cover https and uses procedures. Here is what I use.

public static string RootUrl
{
get
{
HttpContext context = HttpContext.Current;
string executionPath = context.Request.ApplicationPath;
return string.Format("{0}://{1}{2}", context.Request.Url.Scheme,
context.Request.Url.Authority,
executionPath.Length == 1 ? string.Empty : executionPath);
}
}


mikail
on April 29, 2008

hi
while i am using this code


i got an error. like
"Cannot switch views: Validation (Internet Explorer 6): Element 'link' cannot be nested within element 'td'."
i coulndt solve this.
there is any one to known reason?
thanks


mikail
on April 29, 2008

code is here
link rel="stylesheet" href="../js/cbcscbinsmenu.css" type="text/css"


Asbjørn Ulsberg
on June 26, 2008

What I do to solve this, is replace all instances of "~/" in the code with Request.ApplicationPath (after some validation and preparation) in a base class' Render() method. Since "~/" is used elsewhere in ASP.NET and is automatically replaced on all Server Controls, I think this is a pretty neat solution.


Nathanael Jones
on August 9, 2008

I wrote a test suite to figure out exactly how broken 'link', 'meta', and 'script' references are when used with master pages.

Play around with it:
http://nathanaeljones.com/tests/contentplaceholder/

I blogged about the core issue and how to solve it application-wide:
http://nathanaeljones.com/11011_Referencing_stylesheets_scripts_from_content_pages


afterburn
on August 18, 2008

Something i found is ASP.net may try to rewrite some URL's based on the actual url selected instead of the root of the site.

I had to use a ControlAdapter to over ride the form rendering. But that only solved URL's to the page. That didn't fix images and css that try the same thing but couldn't figure out how to get the CSS in the theme to not do ../../../../style.css ; when asp.net actually sees this, it throws an error because the path is actually outside of the website. something about "../" is dangerous and can not go past the root of the web, when in actually it would have only rendered the browser correctly mapping the css theme based on the URL;

Wasn't able to resolve that. So all my themes only contain skins and css is a root directory using a virtual path "/css/style.css" instead. buggy.. found another nice bug that results in iis crashing but no where at MS to report stupid things like that.


Cookston
on November 18, 2008

Hi,

I tried this in a nested master page, the <%= BaseURL %> was evaluated correctly in the nested page, but not the parent master page.

Both master pages inherit from the base class.

It appears as MasterPages/%3C%25=%20BaseURL%20%25%3E in the resulting HTML suggesting that this method cannot be used with nested master pages because all href's are prepended with the MastePage's folder.

My parent master has this:

head runat="server">
title>
link rel="stylesheet" type="text/css" media="screen" href="<%= BaseURL %>/lib/css/mycss.css" />
asp:ContentPlaceHolder ID="ContentPlaceHolderHead" runat="server">
/asp:ContentPlaceHolder>
/head>


The nested master page has this:

asp:Content ID="ContentHead" ContentPlaceHolderID="ContentPlaceHolderHead" runat="Server">
link rel="stylesheet" type="text/css" media="screen" href="<%= BaseURL %>/lib/css/anothercssofmine.css" />
asp:ContentPlaceHolder ID="ContentPlaceHolderHead" runat="server">
/asp:ContentPlaceHolder>
/asp:Content>

(First Left angled bracket removed on each line so this can be posted)

The nested part of the page is evaluates <%= BaseURL %> correctly, but the parent part gets MasterPages/%3C%25=%20BaseURL%20%25%3E

Also, I find that with this method the Design editor mode does not render using the stylesheet when using <%= BaseURL %>

Any ideas?

HL


ctrlnick
on November 26, 2008

Can anyone tell me how to refer to a javascript or css file , which is not copied into my project's local folder.. LIke let us say that the resource files are all on prodcution server. Without having copy of them in my project folder, Can I refer to them in my master pages and get it working smoothly?


Milan Negovan
on December 2, 2008

Something like this should work:

< script type="text/javascript" src="[absolute URL to script file]" >


Zain Shaikh
on January 9, 2009

using the base thingy, does not provide the intellisense... :(


Jeff Mounce
on March 1, 2009

I liked Dave's idea above. Problem is that it only works with pages that do not have dynamically-modified headers.

If you use something dynamic, such as an AJAX extender, which injects tags in the header, it will fail with:

"System.Web.HttpException: The Controls collection cannot be modified because the control contains code blocks (i.e. ).
"

on header.Controls.Add(js);

Bummer. I liked the idea.


Ozzie
on June 3, 2009

Duuuuuuude.... THANKS! I had been slaving over this for a while! Thanks for sharing your knowledge.


Tom Theisen
on September 30, 2009

There is a much easier way. No server controls. No messing with each script or image in your entire master page.

Just put this line in the top of your head

[ base href='[%= ResolveUrl("~") %]' / ]


Just replace the square brackets with angle ones.


virux
on November 12, 2009

Thanks tom,, ur way is quite easy ..