Cutting Edge

Dress Your Controls for Success with ASP.NET 1.1 Themes, Part 2

Dino Esposito

Code download available at:CuttingEdge0406.exe(143 KB)

Contents

The ASP.NET 2.0 Compilation Model
Setting Up a Theme Compiler
Detecting Theme File Changes
Generating the Theme Class
Generating the Dynamic Assembly
Applying the Theme
Adding Support for CSS Styles
Parsing the ASP.NET 2.0 Theme Format
A Word on Reflection
Using the Source Code

Last month I demonstrated an easy way to add theme support to ASP.NET 1.1 applications (see Cutting Edge: Dress Your Controls for Success with ASP.NET 1.1 Themes). I wrote an XML file to represent the theme settings and then I compiled it into a Visual Basic® .NET class and into a C# class using a tool I wrote in Visual Basic. Finally, I added the resulting file to a Web project. Once compiled to a class file, theme settings are used to configure control properties using strongly typed, early-bound code.

You may notice a significant drawback to this model—if you modify anything in the theme file, a new compile step is required to apply and reflect changes. There are a couple of ways to work around this. One is to read from the theme file at run time and use .NET reflection to update any control properties programmatically. No compilation is required, but this would mean that theming would be implemented through late-bound access to controls.

While this doesn't have to slow down your application because it could be designed to happen only once per session, if you're trying to squeeze every little bit of performance out of the code, it's definitely something to avoid. So where can you turn if you don't use .NET reflection?

You could use the ASP.NET runtime built-in mechanism that detects changes to page files and other critical files such as web.config and global.asax. When an .aspx page is modified, its source code is parsed and a class file is generated. The class file is then compiled into an assembly and loaded into the current AppDomain. This procedure takes place dynamically in the background while the application is up and running. As a result, the application promptly detects changes to any of its pages and refreshes them.

You could design a similar mechanism for theme files. Instead of compiling theme files offline, the theme manager component could detect changes to monitored files and create all necessary assemblies on the fly. This model would be more effective because it combines extreme flexibility and early-bound code.

In this column, I'll design and implement a component that parses theme files in memory and builds dynamic assemblies much like the HTTP runtime does for .aspx resources. In addition, I'll briefly discuss the solution based on .NET reflection, which is much easier to code.

The ASP.NET 2.0 Compilation Model

Implementing a form of dynamic compilation in ASP.NET 2.0 will be much easier than it is in ASP.NET 1.1 because the infrastructure exposes a reliable built-in layer. The new build system automatically manages quite a few file types, including theme files. In addition, you can extend it by writing custom build providers to take care of your own resources.

Since its inception, ASP.NET has compiled a few file types on the fly—Web pages (.aspx), Web services (.asmx), HTTP handlers (.ashx), and embedded user controls (.ascx). These files are automatically compiled on demand when first requested by an application. Any changes made to the source of a dynamically compiled file automatically invalidates the corresponding assembly, which will then be recreated. This mechanism greatly simplifies application development as developers only need to save the file and refresh the page to immediately apply changes to the application.

The new ASP.NET build system eliminates the need for an explicit pre-compilation step within the Visual Studio® IDE and provides an extensibility model that allows new file types to be added. By default, C# and Visual Basic class files are detected, as are themes, Web Services Description Language (WSDL) scripts, and XSD schemas for DataSets. WSDL scripts generate Web service proxy classes whereas XSD schemas originate typed DataSets.

In ASP.NET 2.0, there are a few new predefined folders that the build system manages—/Code, /Resources, and /Themes. Their contents are managed and monitored by ASP.NET, which processes files and generates and links assemblies as needed.

Theming and personalization support in ASP.NET 2.0—two features based on dynamic code compilation—are built entirely using the new build provider model. If you're looking for a head start on the ASP.NET 2.0 compilation model, have a look at New Code Compilation Features in ASP.NET Whidbey.

The ASP.NET 2.0 build system is highly customizable and can be extended to support custom files. The key to this change is the <buildProviders> section in the web.config file. The <add> section lets you define a new build provider associated with a given file extension and folder.

<buildProviders> <add extension="*.template" appliesTo="Templates" type="Samples.MyBuildProvider" /> </buildProviders>

A build provider object is derived from the BuildProvider base class. The appliesTo attribute indicates one or more folders that the provider will monitor and manage. In the end, to implement dynamic compilation of certain resources in ASP.NET 2.0 you only need to write and register a build provider. Although writing a build provider may not be trivial, at least you have a clear path to take and the certainty that the surrounding environment fully supports the feature you're going to build.

In ASP.NET 1.1, on the other hand, you're on your own and must generate the code and manage dependencies. At the end of the day, both in ASP.NET 2.0 and ASP.NET 1.1 the main task of the code you're called to write is the same—the generation of a CodeDOM tree. However, in ASP.NET 1.1 you need to build some infrastructure that decides whether to compile or just load from an already existing assembly.

Setting Up a Theme Compiler

Figure 1 outlines the steps needed to build a dynamic theme compiler for ASP.NET 1.1. The ThemeManager class is the connection point between the application and the infrastructure. A client application just calls the Init method of the ThemeManager class, passing in the name of the theme file:

void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { LoadData(); ThemeManager.Init("brown", this); } }

The Init method triggers a procedure that gets an instance of the specific theme class. Once the theme manager holds that object, it applies the theme and all of its associated CSS style sheets. For the purposes of this column, a theme is an XML file that contains code snippets describing property-value pairs for a number of themeable server controls. Figure 2 shows an abbreviated sample theme file.

Figure 2 Sample Theme File

<Flat> <Button> <Property>BackColor</Property> <Value>Color.WhiteSmoke</Value> </Button> <Button> <Property>BorderStyle</Property> <Value>BorderStyle.Solid</Value> </Button> <Button> <Property>BorderWidth</Property> <Value>Unit.Pixel(1)</Value> </Button> <Button> <Property>BorderColor</Property> <Value>Color.Gray</Value> </Button> <Button> <Property>Font.Name</Property> <Value>"Tahoma"</Value> </Button> <TextBox> <Property>Font.Name</Property> <Value>"Tahoma"</Value> ••• <DataGrid> <Property>Font.Name</Property> <Value>"Tahoma"</Value> </DataGrid> <DataGrid> <Property>Font.Size</Property> <Value>FontUnit.Point(8)</Value> </DataGrid> <DataGrid> <Property>BorderStyle</Property> <Value>BorderStyle.Solid</Value> </DataGrid> <DataGrid> <Property>BorderWidth</Property> <Value>Unit.Pixel(1)</Value> </DataGrid> <DataGrid> <Property>BorderColor</Property> <Value>Color.Gray</Value> </DataGrid> </Flat>

Figure 1 Building the Compiler

Figure 1** Building the Compiler **

The initialization of the theme manager passes through a couple of distinct steps, as you can see here:

public static void Init(string theme, Control root) { HandleThemeInfo(theme, root); HandleCssInfo(theme, root.FindControl("Stylesheet")); }

The string parameter is the name of the theme. This name is mapped to a theme file using a fixed naming convention. The Control parameter indicates the root of the control's tree to which the theme will be applied. If you set it to the this keyword (the equivalent of the Me keyword in Visual Basic), it indicates the whole page.

The HandleThemeInfo routine first determines if the theme file has changed since last time. If it hasn't, HandleThemeInfo verifies that the corresponding DLL is available in the ASP.NET temporary folder (the same folder where dynamically compiled pages are stored). If all went well, the Init method attempts to load the assembly and grab a reference to the theme object. If either the DLL is missing or the theme source file has changed, the Init method parses the theme contents and generates a C# (or Visual Basic) class. The class is then compiled into a dynamic assembly and loaded into memory, as you can see in Figure 1.

Detecting Theme File Changes

In Windows®, changes to files and folders can be detected in real time using a built-in system component known as the file notification engine. In the Microsoft® .NET Framework, the component is wrapped by the FileSystemWatcher class. An instance of this class works under the covers of the ASP.NET runtime and monitors changes to all the source .aspx files involved with an application. If you embed a file notification handler in the theme manager class, you will be able to catch all changes to the theme file that occur while the Web application is up and running. But what if you stop the application and then update the theme files? In this case, no changes will be detected unless you persist the timestamp of monitored files. Note that a similar mechanism is also employed by ASP.NET to track changes across restarts of the application. To implement a tracking mechanism for theme files, you can borrow some of the ideas and the solutions used within the ASP.NET HTTP runtime.

ASP.NET stores all of its temporary files in a directory below the Microsoft.NET\Framework\v1.1.4322\Temporary ASP.NET Files folder which is, in turn, rooted in the Windows folder. The Temporary ASP.NET Files folder has as many subdirectories as there are Web applications running. Each of these subdirectories contains a couple of child directories whose names are randomly generated strings. At the end of the path you find all the temp files and assemblies that make the application run. You can programmatically obtain the path to this folder using the following call:

string aspNetTempPath = HttpRuntime.CodegenDir;

The folder you get contains an XML file for each page you access in the application. The file has the same name of the ASPX source, plus some extra information and the XML extension:

Sample.aspx.<em xmlns="https://www.w3.org/1999/xhtml">XXX</em>.xml

The contents look like the following code snippet:

<preserve assem="..." type="..." hash="..."> <filedep name="c:\inetpub\wwwroot\xxx.aspx" /> </preserve>

The hash attribute and the XXX string in the file name contain the timestamp of the page file (the <filedep> node) to which the specified assembly (the assem attribute) refers. Guess what the ASP.NET runtime does at this point? The ASP.NET runtime compares the current timestamp of the dependency (the file in the <filedep> node) with the timestamp persisted in the XML file. If the two match, the assembly is up to date; otherwise, the assembly is obsolete and must be recreated. For effective theming support, you need to implement a similar mechanism. You should note that in ASP.NET 2.0 all of this machinery is still needed, but it is implemented in the new compilation model.

The full source code of the HandleThemeInfo method is shown in Figure 3. If the XML companion file doesn't exist in the temporary folder, the theme file must be parsed and a new assembly must be created. If the companion XML file exists, you read the timestamp of the current assembly and compare it to the current timestamp of the source theme file. The timestamp is expressed as the number of ticks in the DateTime object returned by the LastWriteTime property of the file. If the original theme file is not changed, the HandleThemeInfo method checks if the assembly still exists in the temporary folder. If it does and it's up to date, it gets loaded through the Activator object. File cache change dependencies could also be used to further enhance this process.

Figure 3 HandleThemeInfo

Private Shared Sub HandleThemeInfo(ByVal theTheme As String, _ ByVal root As Control) Dim themeName As String = theTheme.ToLower() Dim currentThemeTypeName As String = GetThemeTypeName(themeName) Dim themeAssembly As String = GetThemeAssemblyName(themeName) Dim infoFile As String = GetThemeInfoFileName(themeName) Dim shouldCompile As Boolean = False Dim currentTheme As Theme = Nothing ' Look for a [theme].xml file in the HttpRuntime.CodegenDir folder If Not File.Exists(infoFile) Then shouldCompile = True End If ' Parse the info-file to extract the assembly name and the file ' dependency If Not shouldCompile Then Dim reader As New XmlTextReader(infoFile) reader.Read() ' on the root <info> Dim themeObjAssembly As String = reader("assem").ToString() Dim themeObjType As String = reader("type").ToString() reader.Read() reader.Read() ' on the <filedep> node Dim themeOrigPath As String = reader("name").ToString() Dim themeTimeStamp As Long = Long.Parse(reader("time").ToString()) reader.Close() ' Compare the timestamp of the XML file and the .theme11 file ' If no match is found, the theme object is recreated Dim fiTheme As New FileInfo(themeOrigPath) If fiTheme.LastWriteTime.Ticks <> themeTimeStamp Then shouldCompile = True End If End If ' For some reasons, the DLL doesn't exist If Not File.Exists(themeAssembly) Then shouldCompile = True End If ' Compile the theme into a class/assembly in the ' HttpRuntime.CodegenDir folder If shouldCompile Then CreateTheme(themeName) End If ' At this point, we can safely load the assembly found at the earlier ' step Dim handle As ObjectHandle = _ Activator.CreateInstanceFrom(themeAssembly, currentThemeTypeName) currentTheme = CType(handle.Unwrap(), MsdnMag.Theme) ' Apply the theme currentTheme.ApplyRecursive(root) End Sub Private Shared Function GetThemeTypeName(ByVal themeName) As String Return String.Format("MsdnMag.{0}_theme11", themeName) End Function Private Shared Function GetThemeInfoFileName(ByVal themeName) As String Return String.Format("{0}\{1}.theme11.xml", HttpRuntime.CodegenDir, _ themeName) End Function Private Shared Function GetThemeAssemblyName(ByVal themeName) As String Return String.Format("{0}\{1}_theme.dll", HttpRuntime.CodegenDir, _ themeName) End Function

Generating the Theme Class

The theme file contains the desired settings for some control properties. These settings must be turned into a class which will then recursively apply them to the specified page. A theme gets into the game when all the controls that make up the page are initialized with their default values. Applying a theme to a control means overriding the properties that are part of the theme after initialization. This is what I did in last month's column and it serves as a summary of how themes work in ASP.NET 2.0. First, a theme class inherits a base abstract class named Theme:

public abstract class Theme { public virtual void ApplyRecursive(Control ctl) { Apply(ctl); foreach (Control child in ctl.Controls) ApplyRecursive(child) } public abstract void Apply(Control ctl) }

I compile this base class in a separate assembly and store it in the Global Assembly Cache (GAC). You can also copy the assembly to a public path like the ASP.NET runtime root folder (the v1.1.4322 directory where all system assemblies reside). Note that an assembly can be copied to the GAC only if it is strongly named. In this month's source code, which is available on the MSDN® Magazine Web site, the BaseTheme project is already configured to generate a strongly named assembly.

The code that parses the theme source file and creates the corresponding class is shown in Figure 4. The parsing stage is pretty simple here because I assume that the theme source file has a DataSet-compliant schema. In the end, parsing is reduced to calling ReadXml on a new DataSet object. The CodeDOM code to create the theme class is nearly identical to the code developed last month. I won't cover its details here; check the May 2004 Cutting Edge installment for more information. The code in Figure 5 gives you an idea of the final results in C#. The theme class is named after the source theme and overrides the Apply method to apply settings to an individual control.

Figure 5 Generated Code

//----------------------------------------------------------------------- // <autogenerated> // This code was generated by a tool. // Runtime Version: 1.1.4322.573 // // Changes to this file may cause incorrect behavior and will be lost // if the code is regenerated. // </autogenerated> //----------------------------------------------------------------------- namespace MsdnMag { using System.Web.UI.WebControls; using System.Web.UI; using System.Drawing; public class brown_theme11 : Theme { public override void Apply(Control ctl) { // // ------------------------------------ // Class:: Button // ------------------------------------ if (ctl is Button) { Button _button = ((Button)(ctl)); _button.BackColor = Color.Brown; _button.ForeColor = Color.Linen; _button.BorderStyle = BorderStyle.Outset; _button.BorderWidth = Unit.Pixel(1); _button.BorderColor = Color.PeachPuff; _button.Font.Name = "Arial"; _button.Font.Bold = true; } // // ------------------------------------ // Class:: TextBox // ------------------------------------ if (ctl is TextBox) { TextBox _textbox = ((TextBox)(ctl)); _textbox.Font.Name = "Tahoma"; _textbox.ForeColor = Color.Maroon; _textbox.BackColor = Color.Linen; _textbox.BorderStyle = BorderStyle.Inset; _textbox.BorderWidth = Unit.Pixel(1); _textbox.BorderColor = Color.Chocolate; } // // ------------------------------------ // Class:: DataGrid // ------------------------------------ if (ctl is DataGrid) { DataGrid _datagrid = ((DataGrid)(ctl)); _datagrid.Font.Name = "Arial"; _datagrid.Font.Size = FontUnit.Point(9); _datagrid.BorderStyle = BorderStyle.Outset; _datagrid.BorderWidth = Unit.Pixel(2); _datagrid.BorderColor = Color.Brown; } } } }

Figure 4 Create Theme Class

Private Shared Sub CreateTheme(ByVal themeName As String) ' Define the language to be used internally to generate ' source/assembly Dim lang As String = "c#" ' or vb ' Check the theme file for existence Dim fileName As String = GetThemeSourceFile(themeName) If Not File.Exists(fileName) Then Throw New ArgumentException(String.Format( _ "The specified theme file ('{0}') _ does not exist.", themeName)) End If ' Read the content of the file to a DataSet ' Assume that the .theme11 file is an XML file that can be read as a ' DataSet ' Each table in the DataSet has two columns: Property, Value Dim themeContents As New DataSet themeContents.ReadXml(fileName) ' Create a class that inherits from BaseTheme Dim sourceFile As String = CodeGenHelper.ParseThemeCode( _ themeName, themeContents, lang) ' Create the XML companion file Dim infoFile As String = GetThemeInfoFileName(themeName) CodeGenHelper.CreateInfoFile(infoFile, themeName, fileName) ' Compile the class to the ASP.NET temp folder Dim comp As CodeDomProvider Select Case lang.ToLower() Case "vb" comp = New VBCodeProvider Case "c#", "cs" comp = New CSharpCodeProvider End Select CompileThemeClass(comp, sourceFile, themeName) Return End Sub ' ***************************************************************** ' Compile the autogenerated theme class Private Shared Sub CompileThemeClass(ByVal comp As CodeDomProvider, _ ByVal source As String, ByVal themeName As String) Dim asmName As String = GetThemeAssemblyName(themeName) If File.Exists(asmName) Then SafeRemove(asmName) End If Dim icc As ICodeCompiler = comp.CreateCompiler() Dim cp As CompilerParameters = New CompilerParameters cp.ReferencedAssemblies.Add("system.dll") cp.ReferencedAssemblies.Add("system.web.dll") cp.ReferencedAssemblies.Add("system.drawing.dll") cp.ReferencedAssemblies.Add("BaseTheme.dll") cp.OutputAssembly = asmName cp.IncludeDebugInformation = False cp.GenerateExecutable = False cp.GenerateInMemory = False Dim results As CompilerResults results = icc.CompileAssemblyFromFile(cp, source) If results.Errors.Count > 0 Then Dim msg As String = "" For Each err As CompilerError In results.Errors msg += String.Format("{0} at {1},{2} {3}", err.ErrorText, _ err.Line, err.Column, vbCrLf) Next Throw New ApplicationException("Can't compile the theme: " + msg) Return End If Return End Function ' ***************************************************************** ' Remove/rename old assemblies to be replaced Private Shared Sub SafeRemove(ByVal asmName As String) ' Try to delete any old copy of the DLL If File.Exists(asmName + ".DELETE") Then File.Delete(asmName + ".DELETE") End If ' Try to delete the DLL Try File.Delete(asmName) Catch ex As Exception File.Move(asmName, asmName + ".DELETE") End Try End Sub

Once you have a source file, you should compile and load it in the current AppDomain. In the sample code for this column, I use C# to generate the interim source code of Figure 5. This is totally arbitrary; you can change it to Visual Basic .NET with a little tweak to the code that is shown in Figure 4:

Private Shared Sub CreateTheme(ByVal themeName As String) Dim lang As String = "vb" ••• End Sub

It is important to note, though, that the language used at this level has nothing to do with the language you use to develop the whole project. You can use C# to generate interim theme classes in a Web application developed with Visual Basic.

The code in Figure 4 controls the creation of the XML companion file, which is needed to track changes to the source theme file. Figure 6 shows how the companion XML is created.

Figure 6 Creating the XML

Public Shared Sub CreateInfoFile(ByVal infoFile As String, _ ByVal themeName As String, ByVal themeFile As String) ' Create the info file ' <info assem="xyz" type="MsdnMag.Sample_Theme11"> ' <filedep name="c:\inetpub\wwwroot\app\themes\sample.theme11" ' time="ticks" /> ' </info> Dim xml As New XmlTextWriter(infoFile, Nothing) xml.Formatting = Formatting.Indented xml.Indentation = 1 xml.IndentChar = vbTab xml.WriteStartElement("info") xml.WriteAttributeString("assem", "theme") xml.WriteAttributeString("type", "MsdnMag." + themeName + "_theme11") xml.WriteStartElement("filedep") xml.WriteAttributeString("name", themeFile) xml.WriteAttributeString("time", New _ FileInfo(themeFile).LastWriteTime.Ticks) xml.WriteEndElement() xml.WriteEndElement() xml.Close() End Sub

Note that you don't need to take any special measures to create, edit, or delete files in the ASP.NET temporary folder (the file returned by the HttpRuntime.CodegenDir property). The reason is that the default ASP.NET account (ASPNET on Windows 2000 and Windows XP and Network Service on Windows Server™ 2003) is always granted full privilege on that folder. If you are impersonating another user, then you should make sure that account can read and write files in that folder. If you don't, themes won't work and the whole ASP.NET application may fail. Note that using the ASP.NET temporary files isn't necessarily a recommended approach in general scenarios, but in this case it makes sense.

Generating the Dynamic Assembly

To programmatically compile a class file, you first get hold of a CodeDomProvider object—specifically, a CSharpCodeProvider or VBCodeProvider object. Next, you must obtain a compiler object by calling the CreateCompiler method, after which you configure the compiler. You reference assemblies, define the output assembly name, choose whether you're compiling to a library or to an executable, and decide whether to include debug information. Basically, you have as many properties and collections to work with as there are switches for the command-line versions of the C# and Visual Basic compilers.

Because you're compiling a class that inherits from a base type, you must ensure that the base type assembly is referenced. In this case, the assembly is BaseTheme, as you saw in Figure 4. A base theme class also exists in ASP.NET 2.0 but it's part of the .NET Framework and as such is brought in by the System.Web assembly.

This code compiles the specified source file into an assembly:

results = icc.CompileAssemblyFromFile(cp, source)

In this month's sample code, I use a fixed convention for the name of a theme assembly—the name of the theme followed by a constant. For example, brown_theme.dll. This is totally arbitrary; you can name the assembly using a randomly generated string if you want. Random names, though, are dangerous because they can lead to a proliferation of useless assembly files, one for each version of the theme you update in the lifetime of the app. Always using the same name ensures that the file will be overwritten.

So, what if you modify the theme while the application is running? In this case, you can't just delete or overwrite the DLL because it is locked by the ASP.NET worker process. But you can rename the file to .DELETE (this is always possible) and create a new one with the same name. Next time the assembly is generated, just get rid of all .DELETE files. The ASP.NET runtime employs a similar procedure to replace its temp files.

Applying the Theme

Now you have an assembly with a theme object that's ready to use. How do you load the assembly and type into memory? Take a look at the following code snippet:

ObjectHandle handle; handle = Activator.CreateInstanceFrom(themeAssembly, currentThemeTypeName); currentTheme = (MsdnMag.Theme) handle.Unwrap();

The CreateInstanceFrom method on the Activator object loads the assembly from the specified path and returns a reference to the specified type—the type of the dynamically created theme class. CreateInstanceFrom doesn't return the direct object, though. It returns an ObjectHandle object. ObjectHandle boxes the actual object into a reference so that the object can be passed between multiple AppDomains more effectively—that is, without loading the object's metadata in each AppDomain. In this case, you don't really take advantage of this feature because a single AppDomain is involved. In order to get the wrapped object, you call Unwrap and cast to the right type. You should note that all overloads of CreateInstanceFrom that accept an assembly path return ObjectHandle objects. So unboxing the theme object is a necessity, rather than a choice.

Once you have obtained a reference to the dynamically created theme object, you cast it to the base class and call the ApplyRecursive method, as seen here:

currentTheme.ApplyRecursive(root)

As shown earlier, ApplyRecursive recursively calls into the overridden Apply method for each control in the specified tree of controls. Figure 7 shows a sample page skinned in two different ways.

Figure 7 Two Skins

Figure 7** Two Skins **

The theme manager component I've discussed so far doesn't require you to bind your app to a fixed number of themes. New theme files can be deployed at all times and are automatically compiled and used on demand when the next page request arrives.

Adding Support for CSS Styles

In ASP.NET 2.0, a theme is made of a skin (control properties) and one or more CSS style sheets. Adding support for CSS styles is also easy in ASP.NET 1.1. Figure 8 lists all the necessary code. To link a CSS style sheet to a Web page you need to define a <link> tag in the <head> section:

<head> <link rel=stylesheet href=... /> </head>

Figure 8 Add CSS Support

Private Shared Sub HandleCssInfo(ByVal theme As String, _ ByVal link As HtmlGenericControl) Dim themeName As String = theme.ToLower() ' Retrieve the CSS file (if any): same name, .css extension Dim cssFile As String = GetCssThemeSourceFile(themeName) If Not File.Exists(cssFile) Then Return End If ' No <head> control provided If link Is Nothing Then Return End If link.Attributes("rel") = "stylesheet" link.Attributes("href") = "themes\" + theme + ".css" End Sub

If you want to add or replace CSS style sheets programmatically, ASP.NET 2.0 comes with a new HTML control named HtmlHead that represents the <head> tag. An instance of this control is automatically created if you add the runat="server" attribute to the tag. Using an instance of the control, the ASP.NET 2.0 theme engine appends as many stylesheet references as needed. A similar trick is used here but involves the <link> tag instead of <head> for the sake of simplicity. The requirement is that any page that can sport a theme includes the following markup code:

<head> <link runat="server" id="Stylesheet" /> </head>

The ID set to Stylesheet is arbitrary, but I preferred to keep it fixed for simplicity. The name of the CSS is fixed too and matches the name of the theme file, but with the .css extension. This aspect of the CSS support in themes is arbitrary and can be easily generalized. In ASP.NET, any tag marked with the runat="server" attribute is transformed in a server control. If a tag-specific control does not exist, the tag is mapped to the HtmlGenericControl class. For example, the <head> tag is an instance of HtmlGenericControl in ASP.NET 1.1 but maps to HtmlHead in ASP.NET 2.0. The <link> element is mapped to HtmlGenericControl:

link.Attributes("rel") = "stylesheet" link.Attributes("href") = "themes\" + theme + ".css"

Using the Attributes collection of the class, you can bind CSS documents dynamically to the page.

Parsing the ASP.NET 2.0 Theme Format

The theme format used so far is radically different from that used in ASP.NET 2.0. I opted for the XML schema in Figure 2 for simplicity. The ASP.NET 2.0 format is smarter and easier to use. In fact, it consists of snippets of ASPX controls markup, as shown in the following lines of code:

<asp:Button runat="server" Font-Bold="true" BorderColor="#585880" BorderWidth="1pt" ForeColor="#585880" BackColor="#F8F7F4" />

Basically, you could design the skin of a control in Visual Studio. Then you could just copy and paste the control's markup to the theme file. Parsing a text file made of a sequence of markup blocks is not as easy as building a DataSet from some XML text. However, with some help from regular expressions and a little effort on your own, you can make it through. It is interesting to note that in this case you can use any new compelling ASP.NET 2.0 themes in your ASP.NET 1.1 applications.

A Word on Reflection

At the beginning of this column I mentioned that themes can also be implemented using reflection, thus avoiding parsing and class compilation. Let me show you a quick trick that can be used to import ASP.NET 2.0 themes in ASP.NET 1.1 using reflection. The prerequisite to the trick is that you own a component that parses the ASP.NET 2.0 format and returns a collection of property/value pairs for each themeable control. Given a page control, the following code sets the BackColor property using the string representation of a value:

Dim desc As PropertyDescriptor desc = _ TypeDescriptor.GetProperties(ctl) _ ("BackColor") Dim c As TypeConverter = desc.Converter Dim value As Object = _ c.ConvertFromString("Cyan") desc.SetValue(ctl, value)

This code snippet is logically equivalent to the following strongly typed code:

ctl.BackColor = Color.Cyan

If you dynamically parse an ASP.NET 2.0 theme file and set control properties through reflection you get the same effect I discussed earlier. If you want to generate code from an ASP.NET 2.0 theme format you must slightly modify the CodeDOM tree built in this column. In particular, you should replace code snippet expressions with code primitive expressions. (See this month's code download.)

Using the Source Code

The code download consists of three projects: BaseTheme, ThemeManager, and a sample Web application. To add theming capabilities to a new application, you add a reference to the ThemeManager which, in turn, requires a reference to BaseTheme. Next, in the Page_Load event, or wherever else it's appropriate in your application, you call the Init method on the ThemeManager class. Any theme file you plan to use must be deployed in a Themes folder below the application's root virtual folder. You should deploy the BaseTheme either in the GAC or in a path where the compiler can locate it. This requirement doesn't apply to ThemeManager, though having both assemblies installed in the same location wouldn't be a bad idea. Both class library projects are configured to output an assembly with a strong name, as shown in Figure 9. The key pair is stored in the BaseTheme folder and the AssemblyInfo file references it.

Figure 9 ThemeManager

Figure 9** ThemeManager **

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is a Wintellect instructor and consultant based in Italy. Author of Programming ASP.NET and Introducing ASP.NET 2.0 (both from Microsoft Press) , he spends most of his time teaching classes on ASP.NET and ADO.NET and speaking at conferences. Reach Dino at cutting@microsoft.com or join the blog at https://weblogs.asp.net/despos.