Click to Rate and Give Feedback
Related Articles

This article reviews the Prism project developed by the Microsoft patterns & practices group and demonstrates how to apply it to composite Web applications using Silverlight.

Shawn Wildermuth

MSDN Magazine July 2009

...

Read more!

Silverlight 2 applications are restricted to running inside a browser. However, Silverlight 3 applications can run inside the browser or out. Here we build a social networking app as a standalone Silverlight 3 application.

John Papa

MSDN Magazine June 2009

...

Read more!

Jeremy Miller continues his discussion of persistence patterns by reviewing the Unit of Work design pattern and examining the issues around persistence ignorance.

Jeremy Miller

MSDN Magazine June 2009

...

Read more!

We demonstrate creating a peer-to-peer processing platform where multiple players function together for a common purpose: getting your work done.

Matt Neely

MSDN Magazine June 2009

...

Read more!

XML comments provide an easy and effective way to document your code. We’ll show you how to use and customize XML comments in your Visual Basic projects.

Lisa Feigenbaum

MSDN Magazine May 2009

...

Read more!

Also by this Author

In this month's installment we build modal and modeless dialog boxes in jQuery and explain how to post data from them to the Web server.

Dino Esposito

MSDN Magazine May 2009

...

Read more!

Thanks to selectors and function chaining, jQuery allows you to write compact, cross-browser code.

Dino Esposito

MSDN Magazine March 2009

...

Read more!

There’s a strong similarity between Web-based Silverlight 2 applications and desktop WPF applications. Enabling easy code reuse between the two is Dino’s focus here.

Dino Esposito

MSDN Magazine October 2008

...

Read more!

This month we begin a look at the Single Page Interface (SPI) model and some design patterns for designing AJAX applications.

Dino Esposito

MSDN Magazine May 2008

...

Read more!

This month Dino Esposito explains how the browser interoperability layer in Silverlight addresses a number of your Silverlight / Web page interaction needs.

Dino Esposito

MSDN Magazine November 2008

...

Read more!

Popular Articles

Now you can perform efficient, sophisticated text analysis using regular expressions in SQL Server 2005.

David Banister

MSDN Magazine February 2007

...

Read more!

Jason Clark

MSDN Magazine July 2003

...

Read more!

The MVP pattern helps you separate your logic and keep your UI layer free of clutter. This month learn how.

Jean-Paul Boodhoo

MSDN Magazine August 2006

...

Read more!

When incorporating the ASP.NET DataGrid control into your Web apps, common operations such as paging, sorting, editing, and deleting data require more effort than you might like to expend. But all that is about to change. The GridView control--the successor to the DataGrid-- extends the DataGrid's functionality it in a number of ways. First, it fully supports data source components and can automatically handle data operations, such as paging, sorting, and editing, as long as its bound data source object supports these capabilities. In addition, ...

Read more!

Paul DiLascia

MSDN Magazine August 2002

...

Read more!

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)
Browse the Code Online
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.
<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 
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.XXX.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.
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.
//-----------------------------------------------------------------------
// <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;
            }
        }
    }
}
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.
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 
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>
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 

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 http://weblogs.asp.net/despos.

Page view tracker