Cutting Edge
Dress Your Controls for Success with ASP.NET 1.1 Themes
Dino Esposito

Code download available at: CuttingEdge0405.exe (138KB)
Contents


Themes and skins are relatively new concepts in the world of Windows®. Windows XP was the first version of Windows to allow users to change the look of their system components. The Microsoft® .NET Framework 1.x does not natively support themes, nor do ASP.NET or Windows Forms applications. In Visual Studio® 2005, on the other hand, themes officially join the arsenal of programming tools, at least for Web developers. In ASP.NET 2.0, page developers can select the page theme using a directive, and all controls will render according to the settings in the theme. The theme in use can also be replaced programmatically at run time. When this happens, the page will reflect the new settings and redraw its controls accordingly.

Themes are a system feature that sit between the core code of the host system (the ASP.NET runtime) and the base implementation of individual components. Ideally, application developers should never need to deal with themes except to select one by name. When a system is built with themes in mind, components detect the current theme and reflect any settings in their UIs. The programmer is not involved in the process and everything takes place at a level below the application's code.

On the other hand, adding theme support to an existing framework not specifically designed to support themes can be a difficult challenge. In this column, I'll demonstrate one way to extend ASP.NET 1.1 controls to support theming. By the time you finish reading, you'll know how to create XML-driven theme files and apply their style attributes to all controls on a page, dressing your controls for success.


The "Theme" of Themes

A golden rule of Web usability states that Web pages on a site must have a consistent look. The use of cascading style sheets (CSS) helps fulfill this requirement, but it is not enough. A CSS file can dictate the appearance of some page elements such as body, anchors, headings, and input fields. ASP.NET controls need the same flexibility that CSS classes provide for visual styles. An ASP.NET theme is like a superset of a CSS file because it addresses all visual properties of the control including those properties that can't be addressed with a CSS style. For example, consider the Button server control. You can set borders, fonts, and colors for a button using either CSS or theme properties. However, the Button class also exposes properties such as CausesValidation and EnableViewState that you can't set using CSS. Those properties aren't visual properties, so it makes sense that they should not be set with a stylesheet. What about a DataGrid then? ItemStyle, ShowFooter, and PagerStyle are all visual properties. How can you set them through CSS?

Unlike Button, the DataGrid control doesn't map conveniently to any of the base HTML elements. This means that any CSS settings can address only a few of its properties (Gridlines, CellSpacing, font, colors) but not all the more abstract and wider set of visual properties supported by this rich control.

A theme is a collection of property settings that are applied to all controls simultaneously with little effort from the developer. The big benefit of themes is that once you have associated a page with a theme, all the controls that you drop on the page surface will automatically take the defined settings—at least at run time.

When are themes applied? This depends on the actual implementation. However, a best practice is to apply theme settings to controls immediately after their instantiation. As a result, each control is first created with the attributes set in the .aspx markup and then assigned the settings stored in the theme file. In this way, theme settings override corresponding markup settings. Unthemed properties retain the original value set in the .aspx file.


A Quick Tour of Themes in ASP.NET 2.0

Before I begin my implementation of themes for ASP.NET 1.1, let's take a quick tour of themes in the upcoming version. In the ASP.NET 2.0 Technology Preview, a theme is described by a .skin file that relies on other resources such as CSS files and images. These files are saved in a special location under the installation path of the .NET Framework 2.0 or under the Themes folder of the application. In the former case, you have global themes visible to all applications running on the machine. Themes stored in an application-specific folder are local to the application and not available outside of it. The path of global themes is: %SystemRoot%\Microsoft.NET\Framework\vx.x.xxxx\ASP.NETClientFiles\Themes. The actual name of the subdirectory labeled vx.x.xxxx changes according to the build of ASP.NET 2.0 that is in use. Under the folder named Themes you'll find multiple child paths, each identifying a distinct theme. The theme's main directory contains .css and .skin files. The name of the directory is the name of the theme. A theme is normally set at design time but, in some cases, can also be set or modified programmatically.

A theme contains skins and CSS styles defined for a variety of server controls. A skin is like a stylesheet for ASP.NET-specific properties. You can use classic CSS styles and skins together. Associating a theme with a page is very easy, as you can see here:

<%@Page Language="C#" Theme="SmokeAndGlass" %>
All you do is assign the name of the theme to the Theme attribute of the @Page directive. Bear in mind that the name of the theme must match the name of a subdirectory under the local or global path for themes. Themes can be set on a per-page basis or automatically assigned to all the pages in the site. To assign them to all pages, you create a setting in the web.config file.

To create a theme, you first create any optional CSS files you plan to use. This allows you to style HTML elements such as tables, anchors, input fields, and so forth. Then you create a skin file in which you define the settings you want to be applied to the ASP.NET server controls. The following code snippet is an ex-cerpt from one of the ASP.NET 2.0 standard themes—the SmokeAndGlass theme:

<asp:TextBox runat="server" 
     BackColor="#FFFFFF" ForeColor="#585880" 
     BorderStyle="Solid" BorderColor="#585880" 
     BorderWidth="1pt" Font-Size="0.9em" 
     Font-Names="Verdana" />
<asp:Button runat="server" 
     Font-Bold="true" BorderColor="#585880" 
     BorderWidth="1pt" ForeColor="#585880" 
     BackColor="#F8F7F4" />
Once you select the SmokeAndGlass theme, any button and textbox controls will render as described (see Figure 1). Note that an ASP.NET 2.0 theme configures the visual properties of a control. Properties that influence the runtime behavior of the control should not be used.

Figure 1 SmokeAndGlass Color Scheme
Figure 1 SmokeAndGlass Color Scheme

The skin file can be written in a variety of ways. In ASP.NET 2.0, it contains chunks of ASPX markup; in my simple ASP.NET 1.1 implementation I'll discuss here, it is an XML file. In general, using ASPX markup simplifies the writing of themes because you can design your control in Visual Studio .NET and then just cut and paste the corresponding markup to the skin file. However, this requires a parser for the ASPX markup code, which is not publicly available to developers and which would need to be written by hand. For now, I'll express an ASP.NET 1.1 theme file as raw XML.


Designing Themes in ASP.NET 1.1

The major components of themes in ASP.NET 1.1 are an XML file to skin controls, an autogenerated proxy file created with a command-line tool, and an assembly that provides the base class for the proxy along with a couple of static methods that are used to initialize the theme engine.

As mentioned, a theme represents a collection of assignments in which the left part is a control property and the right part is a typed value. These settings are read from the theme file and are known only at run time. As it turns out, you have two basic ways to implement that—using reflection or via a dynamically generated theme object. Personally, I have a love-hate relationship with .NET reflection. I love the flexibility that reflection provides, as much as I dislike the messy code it forces you to write. Reflection-based code is often hard to read and maintain if you use it for purposes other than checking the metadata and the types of a given assembly. To set a property on a control using reflection, you must first get a PropertyDescriptor object for the property, then get a typed value for it, and make the assignment. However, consider the following snippet:

<button>
   <property>BackColor</property>
   <value>Color.Gainsboro</value>
</button>
You know the property name—BackColor—and a string that represents the value to assign. Using reflection, you must be able to transform "Color.Gainsboro" into an object of type Color. It is possible, but it is not trivial.

Let's explore the alternate approach—code generation. Code generation consists of generating a Visual Basic® or C# class that parses the theme file and produces instructions like the following:

ctl.BackColor = Color.Gainsboro;
Parsing is simpler and there's no runtime overhead because of code indirection and late binding. In the .NET Framework 1.x, proxy classes for Web services provide a good example of static code generation. When you add a Web reference to your project, Visual Studio .NET generates a source file and silently adds it to the project. When you compile the project, that file is compiled and merged with the resulting assembly. On the downside, you need to regenerate the class whenever the Web Services Description Language (WSDL) file changes.

A third possibility would be dynamic code generation. Dynamic code generation entails parsing and compiling the theme file at run time in much the same way as ASP.NET pages are processed by the runtime. This is how themes are implemented in ASP.NET 2.0 (an assembly for the referenced theme is generated on the first request and used for all subsequent requests). I'll show how to implement this mechanism in ASP.NET 1.x in a future column. For now, let's go for static code generation Web-services style.

To support themes in ASP.NET 1.1, you need to create an XML document that represents the skin of the various controls, write a command-line tool that creates a Visual Basic or C# source class out of the theme, and create a theme manager object to ensure that all controls in the page are properly skinned.


Creating Themes

A theme is an XML document with a schema that can be mapped to a DataSet object. The theme name is the DataSet name and each table stores the settings for a particular control. Figure 2 presents a sample theme that skins Button, TextBox, and DataGrid controls. You can save this file to any location on the server machine—for example, in a Themes folder below the root of the Web application.

Settings in the theme file will be processed by a theme manager object—a custom class generated by parsing the theme file. This class will contain code like the following for each skinned control:

If TypeOf(ctrl) Is Button
   ctrl.BackColor = Color.Gainsboro
   •••
End If
During the page loading, the theme manager will invoke this class to override the default settings of the control, thus ensuring that each skinned control has the expected graphical attributes.


.NET Code Generation

The System.CodeDom namespace contains a bunch of classes that you can use to easily create a code graph to render in either Visual Basic .NET or C#. You outline the structure of the code you want to obtain and then render it in the language of choice. The System.CodeDom classes let you indicate the type of statement you want and its parameters. When it comes to generating code, that abstract description is turned into the real syntax.

Figure 3 illustrates the source code of the command-line utility used to parse theme files into a C# or Visual Basic class. The command line accepts up to three arguments—the file name, the language (which defaults to Visual Basic), and the namespace (which defaults to MsdnMag). A really simple CmdLineParser class is used to parse the arguments. If you're looking for a more complete one, take a look at Command Line Parser Library.

The XML theme file is assumed to be in a format that can be easily turned into a DataSet object using the ReadXml method:

Dim themeContents As New DataSet
themeContents.ReadXml(params.FileName)
In addition to the schema of Figure 2, you can use the following syntax, which is more compact:
<Flat>
  <Button Property="BackColor" Value="..." />
  <Button Property="ForeColor" Value="..." />
  •••
  <DataGrid Property="Gridlines" Value="..." />
  •••
</Flat>

The theme compiler reads those settings in and produces a new class named after the root node of the theme. If the theme is rooted in a node named Flat, the resulting class will be FlatTheme. This class will inherit from a Theme base class defined in a companion assembly (more on this in a moment).

Public Class FlatTheme : Inherits Theme

The class overrides one of the methods on the base class—Apply. The following is the signature of the Apply method:

Public Overrides Sub Apply(ByVal ctrl As Control)
The Apply method takes a control and compares its actual type against all the skinned controls in the current theme. If the type of control matches one of the skinned types, all the settings defined in the theme are applied to the control.

Figure 4 shows the source code of the method that governs the code generation process. The CodeCompileUnit object represents the root of the code graph you are going to generate. You start by adding a namespace (an instance of the CodeNamespace class) and then all the necessary imports. You add the namespace to the graph using the Namespaces collection, like so:

Dim ns As New CodeNamespace(info.UserData.NsName)
unit.Namespaces.Add(ns)

An import is represented by the CodeNamespaceImport class. You add it to the Imports collection of the namespace object:

Dim imp As New CodeNamespaceImport("System.Web.UI")
ns.Imports.Add(imp)
The class must be added to the Types collection of the namespace. Any base type for the class (including, of course, interfaces) is added to the BaseTypes collection of the CodeTypeDeclaration class. In Visual Basic .NET, the first element is associated with the Inherits keyword; all the others are associated with the Implements keyword. The code snippet here shows how to generate the declaration of a class that contains a method:
Dim cls As New CodeTypeDeclaration(clsName)
ns.Types.Add(cls)
cls.BaseTypes.Add("Theme")
Dim methodApply As New CodeMemberMethod
cls.Members.Add(methodApply)

For methods, you need to specify formal parameters and the function body. Here's how to declare the Apply method's signature:

met.Attributes = MemberAttributes.Public Or MemberAttributes.Override
met.Name = "Apply"
met.Parameters.Add( _
    New CodeParameterDeclarationExpression("Control", "ctrl"))
The body is a collection of variable declarations and statements. The idea is that you generate a comment and an If statement for each table in the DataSet. Remember that a table corresponds to a control to be skinned where the table is the class name of the control. You'll find all the code that generates the FlatTheme class in the code download on the MSDN® Magazine Web site. Again, the underlying theme file was shown in Figure 2.

The System.CodeDom namespace lets you declare the code you want to be generated. However, it doesn't cover all possible instructions you might want to write. For example, there's no way to obtain a For...Each loop. The namespace provides iteration capabilities but only through the Do...While statement. (Iterative commands are all equivalent if you look at the core functionality, but each has a different degree of usefulness, depending on the scenario.) Once the code graph is complete, you can generate the output in either Visual Basic .NET or C# (or any other language that has an ICodeGenerator implemented for it). Figure 5 shows the code that creates the source class.


The ThemeManager Class

The mechanism just described is quite similar to the "Add a Web Reference" feature in Windows Forms and Web Forms projects. When you choose to add a Web service, Visual Studio .NET generates a source file and binds that file to the project. Likewise, you generate the theme class and add it to an ASP.NET project.

Basic operation and interaction with any control tree is managed through the code in the theme class. Despite the warning at the top of the autogenerated file, you can edit the file as long as you know what you're doing.

The dynamically generated class inherits from a base class—Theme. The Theme class is part of an external assembly that you need to bind to your main application. The source code of the Theme class is shown here:

Public MustInherit Class Theme
  Public Overridable Sub ApplyRecursive(ByVal ctl As Control)
     Apply(ctl)
     For Each child As Control In ctl.Controls
         ApplyRecursive(child)
     Next
  End Sub
  
  Public MustOverride Sub Apply(ByVal ctl As Control)
End Class
It defines an ApplyRecursive method that loops through all controls in the specified container and skins each individually. The Apply method is abstract and is properly overridden in the theme class generated by the theme compiler.

Which component is physically responsible for applying the selected theme? The theme manager is a class that contains a handful of static methods to register multiple themes with a single application and to select a particular theme to apply.

The theme manager has two main methods—Register and Apply. The Register method takes the name and type of the theme and creates an instance of the specified class. The object is then placed in the ASP.NET cache with an autogenerated key. The Apply method takes the name of the theme, retrieves the object from the cache, and calls ApplyRecursive. In turn, ApplyRecursive invokes the object's specific Apply method, which was tailor-made for the theme XML source file in question:

Shared Function Register(name As String, t As Type) As Theme
   Dim key As String = GetCacheKeyName(name)
   Dim obj As Theme = Activator.CreateInstance(t)
   HttpContext.Current.Cache(key) = obj
End Function

Shared Function Apply(name As String, root As Control)
   Dim key As String = GetCacheKeyName(themeName)
   Dim obj As Theme = HttpContext.Current.Cache(key)
   obj.ApplyRecursive(root)
End Function

The elements of the puzzle are all set now. I'll briefly summarize the steps you need to take to extend ASP.NET 1.1 applications with themes. You start by storing property settings into an XML file and then parse this file into a Visual Basic .NET or C# class. The class must be added to the project; one class for each theme you want to support. Obviously, you could write the class files directly without using the tool.

Once the theme classes are part of the project, you modify the Page_Init handler as follows:

Sub Page_Init(sender As Object, e As EventArgs) Handles MyBase.Init
   If Not IsPostBack Then
      ThemeManager.Register("Sample", GetType(MsdnMag.SampleTheme))
      ThemeManager.Register("Flat", GetType(MsdnMag.FlatTheme))
      ThemeManager.Apply("Sample", Me)
   End If
End Sub
You register the theme objects and apply the one of your choice. If you don't allow users to change the theme at run time, then you have no need to register more than one theme class. Note that Visual Studio .NET hides the Page_Init handler in the Web designer-generated region.

To change the theme at run time, add a dropdown list to your page and fill it with all the available themes. (You can also add a new method to ThemeManager to return them all.) Turn on the AutoPostBack attribute of the dropdown list so that the page posts back whenever the user makes a new selection. The event handler will contain the following, surprisingly simple, code:

ThemeManager.Apply(ListOfThemes.SelectedValue, Me)

I recommend that you bind the data again after you change the theme. I also suggest that you do whatever you do in the Page_Load handler that relates to the controls subject to skins. This is especially important if you skin data-bound controls like the DataGrid. Figure 6 shows a sample page.

Figure 6 Changing a Theme with a Dropdown List
Figure 6 Changing a Theme with a Dropdown List

Issues and Gripes

As you may have guessed already, the Web theming solution I've shown is not right for everyone and is primarily targeted toward using just one theme across all pages of a Web site. The first issue is the syntax of the theme file. The XML document is tedious to cre-ate and must be turned into a source class before use. As a shortcut, you can use the XML file to create the source class and then update the Visual Basic .NET or C# file directly in case of further changes.

Since you bring in plain classes, debugging is not an issue. Visual Studio .NET can easily step into the theme classes as in any other class in the project. I've created a registry file to add some shell support to theme files. If you install the shell support, you can then right-click on a theme file and create the corresponding source file in the preferred language (see Figure 7).

Figure 7 Compiling a Theme
Figure 7 Compiling a Theme

Another subtle issue has to do with the viewstate restoration and applies if you change themes dynamically. Suppose you support two themes, Blue and Green, where the Blue theme doesn't contain any background color for textboxes. Suppose, also, that Blue is the default theme. The first time you display the page, the background of textboxes is white—or any other color you set in the designer. When you change the theme to Green, the background color changes accordingly. At this point, if you move back to the original theme—Blue—the textbox retains the background color of the Green theme. Why? Because of the viewstate restoration and because no explicit background color setting was forced to the controls by the current theme.

In my themes implementation for ASP.NET 1.x, you change the theme using a postback event after the page loading stage has completed. This will give the theme the last word on control settings. However, it is important that each theme assigns a value to each property it cares about; otherwise, the value of these properties might change from time to time.


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



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


© 2007 Microsoft Corporation and CMP Media, LLC. All rights reserved; reproduction in part or in whole without permission is prohibited.