RSS 2.0
Journal / Blog
Tuesday, April 03, 2007
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)
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#:

using System;
using System.Data;
using System.Security.Permissions;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;
using System.Collections;

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.
Friday, May 18, 2007 10:28:00 AM (GMT Standard Time, UTC+00:00)
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.
Monday, May 21, 2007 1:28:30 PM (GMT Standard Time, UTC+00:00)
@Dave

Thanks for the post... Your proposed solution is a nice work around, but it doesn't resolve all the problems that Themes present - it still prevents the use of Microsoft Conditional Comments, CSS Media Types, Media Dependent Cascades, and aside from alphanumeric ordering of CSS files in Themes it doesn't allow us to define a Loading Order.

It could also be a bit of a maintenance issue - imagine 2 years down the road, we're at ASP.NET version 5.0 (with possibly a more robust version of Themes), a maintenance programmer delves into your code to change the Styles, this programmer knows CSS and knows ASP.NET 5.0, but they somehow overlook your subdirectory work around.

This is a pretty quick solution - just copy the source above into a file in your App_Code directory and presto.
Wednesday, June 13, 2007 9:36:14 AM (GMT Standard Time, UTC+00:00)
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)
Will
Friday, June 22, 2007 11:53:52 PM (GMT Standard Time, UTC+00:00)
@Will

You're right this solution is useless in your case. Steer clear from ASP.NET Themes! :)

For more information on the limitations of VirtualPathProviders see the MSDN: VirtualPathProvider Class: http://msdn2.microsoft.com/en-us/library/system.web.hosting.virtualpathprovider.aspx
Comments are closed.
Page rendered at Tuesday, January 06, 2009 8:23:32 AM (GMT Standard Time, UTC+00:00)