Home > .NET, ASP.NET, CSS, Themes and Skins > A Resolution to The Problems with Themes, Skins, and Cascading Style Sheets (CSS) – Putting the Cascades back into ASP.NET 2.0 Themes (taking control over CSS Cascades / Load Order, Media Types, and Overrides)

A Resolution to The Problems with Themes, Skins, and Cascading Style Sheets (CSS) – Putting the Cascades back into ASP.NET 2.0 Themes (taking control over CSS Cascades / Load Order, Media Types, and Overrides)

April 2nd, 2007

ASP.NET 2.0 Themes have a couple design flaws, all of which center around the way Themes reference Cascading Style Sheets (CSS). I’ve been posting Theme and CSS related issues and work arounds as I encountered them, and in one of my first posts I outlined the possibility of using a VirtualPathProvider to ignore the CSS files within a directory. In this post I share David Marzo’s implemented of this solution. This resolution resolves most of the issues surrounding Themes and CSS, and essentially puts the Cascades back into CSS.

The Problem / Question:
ASP.NET 2.0 Themes automatically includes all the Style Sheets (.css files) found in the active Theme (App_Theme) directory into the Head of the rendered document. This severely limits the robust CSS language, and works against the last decade of Cascading Style Sheet progress. For more specific details on the problems with ASP.NET 2.0 Themes start reading the articles found here.

A Solution / Work Around:
Add a custom VirtualPathProvider to ignore the Style Sheets (CSS files) found in the App_Theme directory. For more background on this solution see my article and David Ebbo’s article titled Overriding ASP.NET combine behavior using a VirtualPathProvider.

An example of the problem:

The directory structure:

Notice all the Style Sheets (.css files), there are more in the Adapters directory too.

Before including the CustomVirtualPathProvider (code provided below) into the project’s build – the XHTML rendered by Themes:

<html xmlns="http://www.w3.org/1999/xhtml" >
  <head><title>The Problems With Themes and Skins in ASP.NET 2.0</title>
    <link href="App_Themes/Default/CSS/Adapters/ChangePassword.css" type="text/css" rel="stylesheet" /> 

    ... The other 17 externally linked Style Sheets go here, they were removed to improve readability ...

     <link href="App_Themes/Default/CSS/PrinterFriendly.css" type="text/css" rel="stylesheet" />
  </head>
<body>

The Style Sheets are automagically inserted into the Head tag from the active Theme directory. Keep in mind that Internet Explorer has a 30 Style Sheet limitation (see article Q262161).

After including the CustomVirtualPathProvider (code provided below) into the project’s build – the XHTML rendered by Themes:

<html xmlns="http://www.w3.org/1999/xhtml" > 
  <head><title>The Problems With Themes and Skins in ASP.NET 2.0</title>
  </head>
<body>

Notice the complete lack of Style Sheets – ahhh simplicity is bliss, the very foundations of CSS Zen Enlightenment… :) Now we can manually include our Style Sheets, use Conditional Comments, and so on.

The source code for the CustomVirtualPathProvider provided by David Marzo in C#:

namespace Arfila.Web.Logic {
 
 [AspNetHostingPermission(SecurityAction.Demand,
   Level = AspNetHostingPermissionLevel.Medium)]
 [AspNetHostingPermission(SecurityAction.InheritanceDemand,
   Level = AspNetHostingPermissionLevel.High)]
 public class CustomVirtualPathProvider : VirtualPathProvider {
 
   public static void AppInitialize() {
     HostingEnvironment.RegisterVirtualPathProvider(new CustomVirtualPathProvider());
   }
 
   public CustomVirtualPathProvider() : base() { }
 
   private bool IsThemeDirectory(string virtualPath) {
     String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
     return checkPath.StartsWith("~/App_Themes/", 
       StringComparison.InvariantCultureIgnoreCase);
   }
 
   public override VirtualDirectory GetDirectory(string virtualDir) {
     if (IsThemeDirectory(virtualDir)) {
       return new ThemeDirectory(Previous.GetDirectory(virtualDir));
     }
     else {
       return Previous.GetDirectory(virtualDir);
     }
   }
 }
 
 [AspNetHostingPermission(SecurityAction.Demand,
   Level = AspNetHostingPermissionLevel.Minimal)]
 [AspNetHostingPermission(SecurityAction.InheritanceDemand,
   Level = AspNetHostingPermissionLevel.Minimal)]
 public class ThemeDirectory : VirtualDirectory {
 
    VirtualDirectory _toFilter;
    private ArrayList _children = new ArrayList();
    private ArrayList _directories = new ArrayList();
    private ArrayList _files = new ArrayList();
 
    public override IEnumerable Children {
      get { return _children; }
    }
 
    public override IEnumerable Directories {
      get { return _directories; }
    }
 
    public override IEnumerable Files {
      get { return _files; }
    }
 
    public ThemeDirectory(VirtualDirectory toFilter) : base(toFilter.VirtualPath) {
      _toFilter = toFilter;
      BuildChild();
    }
 
    private void BuildChild() {
      foreach (VirtualDirectory dirToFilter in _toFilter.Directories) {
        ThemeDirectory themeDir = new ThemeDirectory(dirToFilter);
        _children.Add(themeDir);
        _directories.Add(themeDir);
      }
 
      foreach (VirtualFile fileToFilter in _toFilter.Files) {
        string fileExtension = VirtualPathUtility.GetExtension(fileToFilter.VirtualPath).TrimStart('.');
        if (string.Compare(fileExtension, "css", true) != 0) {
          _children.Add(fileToFilter);
          _files.Add(fileToFilter);
        }
        else {
          //archivo .css no incluir
        }
      }
    }
  }
}

Some of the code has been modified for readability, download the original source code here.

Note: In order to use this VPP you’ll have to copy the code above into a new class in your App_Code directory.

Caveat: If a Web site is precompiled for deployment, content provided by a VirtualPathProvider instance is not compiled, and no VirtualPathProvider instances are used by the precompiled site. – Taken from the article titled VirtualPathProvider Class on MSDN.


Conclusion:
This is one of the nicest work arounds or resolution to the issues surrounding ASP.NET 2.0 Themes. It allows us to leverage the power of ASP.NET 2.0 default Skins, allows us to logically group design related resources (Style Sheets, images, default Skins, videos, etc…) in the App_Theme directory, allows us to control the loading order (cascades) of style sheets, allows us to use Conditional Comments, to define Media Types, to override / inherit Styles, and to continue using CSS as it’s intended. In addition we can now easily integrate the ASP.NET 2.0 CSS Friendly Control Adapters into our web applications.

Author: Adam Kahtava Categories: .NET, ASP.NET, CSS, Themes and Skins Tags:
  1. May 18th, 2007 at 06:08 | #1

    This is all very clever and technical. For a much quicker solution, I find that putting css files into subfolders of the theme folder makes them load after the css files in the root of the theme folder.

    I assume that this is iterative so you could dictate the load order by changing the folder structure.

    The bad part is that it can screw with image links so best to include images referenced by a stylesheet in the same folder as the css or a subfolder of it.

  2. Adam
    May 21st, 2007 at 06:10 | #2

    @Dave, you’re right this solution is overly technical and clever. An even quicker solution to resolving the issues with Themes and Skins would be to not use them! :)

  3. Will
    June 13th, 2007 at 06:11 | #3

    This solution is actually totally useless for anyone who uses pre-complied web applications, I would think this is a vast majority of developers deploying in a commercial environment. This is a line from MSDN about VirtualPathProviders

    “If a Web site is precompiled for deployment, content provided by a VirtualPathProvider instance is not compiled, and no VirtualPathProvider instances are used by the precompiled site.”

    So basically MS have provided a mechanism that is totally useless for a large number of developers. (referring to both Themes and VirtualPathProviders)

  4. Adam Kahtava
    June 22nd, 2007 at 06:12 | #4

    @Will

    You’re right this solution is useless in your case.

    For more information on the limitations of VirtualPathProviders see the MSDN: VirtualPathProvider Class

  5. Ivan Mercedes
    April 9th, 2010 at 07:36 | #5

    Three years later and we still have the same pains. Adam, as you put it in another post – http://adam.kahtava.com/journal/2006/11/08/the-problems-with-themes-skins-and-cascading-style-sheets-css-where-it-all-falls-apart/:

    A rough list of CSS features compromised by Themes in ASP.NET 2.0:

    1. CSS media types
    2. The CSS @Import rule
    3. The CSS @Media rule
    4. Preferred and alternate style sheets
    5. Media-dependent cascades
    6. Inheritance and cascading
    7. The use of Microsoft’s Conditional Comments

    The solution I’ve found to work pretty well is to create an import.css file in the App_Themes/{skin}/ folder with css @import statements and import each css file in sequence specifying the media type attribute. For example, my import.css file looks like this:

    /* Screen */
    @import url(“/common/css/forms.css”) screen;
    @import url(“/common/css/theme/blueoffice/style.css”) screen;

    /* Print */
    @import url(“/common/css/theme/blueoffice/print.css”) print;

    /* Handheld */
    @import url(“/common/css/theme/blueoffice/handheld.css”) handheld;

    This alternative touches on each of the points above with the exception of #7, which I’ve placed in a root (non-nested) master page. At present, the IE css hacks are pretty small so adding @media declarations within those css files is still a good option.

    So, my conclusion is that there’s still benefit in using ASP.NET Themes while regaining control of the CSS. I believe the App_Themes folder structure isn’t the place for CSS files but the power of the technology coupled with this alternative at least ensures that with changing the theme of your site, you will easily change the corresponding css styles.

  6. Adam Kahtava
    April 11th, 2010 at 08:49 | #6

    @Ivan,

    It sounds like your approach solves your problem nicely.

    There are a couple caveats to using the @import statement though.

    1. Using @import takes longer to load and render the page – the browser loads the Import.css, then waits for each @import stylesheet to load, whereas linked stylesheets are loaded concurrently which renders the page quicker. More information can be found here: don’t use @import.
    2. @import is not supported by browsers older that IE 6 or Netscape 4
    3. The combination of an Import.css file buried within the ASP.NET Theme and Skin structure coupled with the dependency on the @import statement sounds like to another clever hack to get around the fatal flaws surrounding ASP.NET Themes and Skins. :)

    Front-end developers and web designers should be able to just do their job – be creative. They shouldn’t worry about all the constraints and hacks their environment has placed around them. The downsides of ASP.NET Themes and Skins out weight the benefits. I don’t recommend them.

    Thanks for the feedback!

  7. Ivan Mercedes
    April 12th, 2010 at 13:03 | #7

    @Adam

    Can’t argue with that although I’m not 100% sure a “stay away from this technology” attitude is the right choice.

    1. Given the current size of the style-sheets on the application I’m testing this particular approach on, the pages render quickly “enough”.
    2. If you control the environment, as is generally the case with intranet applications, then this isn’t much of a constraint.
    3. I have mixed feelings here because while I agree that a designer shouldn’t be constrained by the technology used, she should be able to understand and leverage its power. Dynamically/programmatically changing the theme/skin of a site could be a very useful thing and so using ASP.NET Themes could be essential. In such a case, placing static link tags wouldn’t allow this dynamic behavior and adding code-behind logic to control the header section feels ugly (except when the framework does it!).

    I say a lot of what we do when the rubber hits the road are workarounds. There comes a time when it’s not about the best approach but the least worst. Overall, I say we should understand the technology well enough to make smart decisions but understand that inevitably, we will need to compromise and finally, be agile enough to adapt accordingly.

    Thanks for the medium and your response.

  8. Adam Kahtava
    April 13th, 2010 at 07:32 | #8

    @Ivan,

    I don’t want to exude a “stay away from technology attitude”. I like technology, but every technology / framework / programming language has its good parts and bad parts (mistakes). I think Themes and Skins are a bad part of ASP.NET, just like the JavaScript’s dependency on global variables is bad. We should tread lightly (be careful) in those areas, not exclusively stay away – there are better alternatives.

    If Themes and Skins solve a problem (are good enough) on low traffic internal intranet applications where we control the environment, then we can certainly make use of them. If we’re working on a site that’s public facing, with ambitions to scale, must perform well, and we’re working with Web Developers / Web Designers that don’t necessarily live in the ASP.NET world, then another alternative is probably in order.

    Anyhow; this is just my opinion. :)

  1. No trackbacks yet.