Dentaku

Per Zimmerman's personal blog

« Back

Take control over stylesheet order and media when using ASP.NET 2.0 themes

Themes in ASP.NET 2.0 is a great concept, but it doesn't give you any control over how stylesheets are added to your pages. ASP.NET won't let you use multiple stylesheets with different media types in a arbitrary order. But you can easily fix it if you want to. I've put together an example which let's you use both themes and have full control over the stylesheet rendering.

The way themes works is that ASP.NET includes all .css files found in the theme folder into you page. The stylesheeets are added as runat at server link elements (HtmlControls with tagname "link"). They're added ordered by their names. The links won't get any media-attribute.

You can read more about the themes problems in Adam Kahtava's post: The Problems with Themes, Skins, and Cascading Style Sheets (CSS) - Where it all Falls Apart

A simple example

You want your web pages to look nice when you print them. The way to do it is to add a stylesheet with media="print" and put it after the default stylesheet. If you don't use themes you could add this to the page's head:

ASP.NET
1
2
3
<link type="text/css" href="StyleSheet.css" rel="stylesheet" media="all" />
<link type="text/css" href="Print.css" rel="stylesheet" media="print" />

Print.css could include rules for hiding elements like navigation and banners that should not be visible when you print it.

If you use themes and the stylesheets are in the theme folder. The output for your page would be like:

ASP.NET
1
2
3
<link type="text/css" href="App_Themes/TheTheme/Print.css" rel="stylesheet" />
<link type="text/css" href="App_Themes/TheTheme/StyleSheet.css" rel="stylesheet" />

If Print.css hides any element, that element won't be visible at all because the StyleSheet.css is rendered after Print.css.

Take control

I made a simple user control to put in the head. It's just a sub class to PlaceHolder so you can add any controls you like in it. I've assumed that stylesheets are written as literal content and when the control prerenders it replaces %Theme with the actual theme path. Put this code in the head of the page:

ASP.NET
1
2
3
4
5
6
7
8
<head runat="server">
    <title></title>
    <cc1:Styles ID="Styles1" runat="server" ThemeVariableName="%Theme">
        <link rel="Stylesheet" type="text/css" href="%Theme/StyleSheet.css" media="all"/>
        <link rel="Stylesheet" type="text/css" href="%Theme/Print.css" media="print"/>
    </cc1:Styles>
</head>

How does it work?

The control does two things in the prerendering:

  1. Disable default rendering
    The page's header must be set runat="server" when you use themes. Therefore it's possible to access it's control collection. You could easily remove any server side links from the head in the page's onPreRender event. But it's not possible to do that from a user control. So instead of removing the links, we make them invisible
  2. Replace %Theme with the actual theme path
    Literal text within a PlaceHolder is added as LiteralControl's. I use a regular expression to replace all occurance of %Theme in LiteralControls to the actual theme path.

Source code

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Text.RegularExpressions;

namespace Dentaku.Web.UI {

    [DefaultProperty("ThemeVariableName")]
    [ToolboxData("<{0}:Styles runat=\"server\"></{0}:Styles>")]
    [Themeable(true)]
    public class Styles : PlaceHolder {

        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("%Theme")]
        [Localizable(false)]
        [Description("Name of the variable that should be replaced by the actual theme path")]
        public string ThemeVariableName {
            get {
                String s = (String) ViewState["ThemeVariableName"];
                return ((s == null) ? "%Theme" : s);
            }

            set {
                ViewState["ThemeVariableName"] = value;
            }
        }
        
        /// <summary>
        /// Fix controls before render
        /// </summary>
        protected override void OnPreRender(EventArgs e) {
            base.OnPreRender(e);

            if (this.Visible) {
                // Hide any server side css 
                foreach (Control c in this.Page.Header.Controls) {
                    if (c is HtmlControl && ((HtmlControl) c).TagName.Equals("link", 
                                StringComparison.OrdinalIgnoreCase)) {
                        c.Visible = false;
                    }
                }

                // Replace ThemeVariableName with actual theme path
                Regex reg = new Regex(ThemeVariableName, 
                                      System.Text.RegularExpressions.RegexOptions.IgnoreCase);

                foreach (Control c in this.Controls) {
                    if (c is LiteralControl) {
                        LiteralControl l = (LiteralControl) c;
                        l.Text = reg.Replace(l.Text, this.ThemePath);
                    }
                }
            }
        }
        
        /// <summary>
        /// Get the theme path
        /// </summary>
        public string ThemePath {
            get {
                return String.Format("{0}/App_Themes/{1}",
                                     this.Page.Request.ApplicationPath,
                                     this.Page.Theme);
            }
        }
    }
}

Skin it

Do you need different number of stylesheets for each theme? You can use skin-files for the Styles control. But if you do, you might want to use skins for all themes. Any text within the Styles control in the page's header will be rendered along with any text set in the skin file. Example what to put in your skin file:
ASP.NET
1
2
3
4
5
<cc2:Styles runat="server">
    <link rel="Stylesheet" type="text/css" href="%Theme/StyleSheet.css" media="all"/>
    <link rel="Stylesheet" type="text/css" href="%Theme/Print.css" media="print"/>
</cc2:Styles>

2007-01-30 13:32:00

2007-02-06 03:37:00 From: Adam Kahtava

That's a lot of code just to include a media type for an external style sheet, but it's still a nice work around. Just a note you spelled my name wrong it's Adam not Adamin . I look forward to your future posts. -Adam

2007-02-06 09:42:00 From: Per Zimmerman

Adam: My apologies. I've corrected your name. I don't agree that it's a lot of code. At least not the code to put in the head. The user controls should be put in a control library. If you put in your example with conditional IE styles and @import, the extra style control around it all is not a lot.

2007-02-12 01:56:00 From: Adam Kahtava

I'm not sure what mean when you say: "You can use skin-files for the Styles control." Could you provide an example? Also it would be nice if you showed the rendered HTML that your control produces. Thanks again -Adam

2007-05-24 17:01:00 From: Vasco

If you don't want so much code add this into your Aspx page Code behind: protected override void OnLoad(EventArgs e) { base.OnLoad(e); // Add the Print.css with media="print" Literal l = new Literal(); l.Text = "< link href=\"" + Request.ApplicationPath + "/App_Themes/print.css\" rel=\"stylesheet\" type=\"text/css\" media=\"print\" / >"; Page.Header.Controls.Add(l); }