Site Skinning

Rich XML Classes Let Users Personalize Their Visual Experience on Your ASP.NET Site

Harry Pierson

Code download available at:SiteSkinning.exe(216 KB)

This article assumes you're familiar with ASP.NET, C#, and XSLT

Level of Difficulty123

SUMMARY

One way that Web sites and applications become better able to meet the needs of customers is by allowing them to personalize their experience. For Web sites, this means displaying the content as the user wants to see it. For rich-client applications, this often means allowing the user to choose the user interface through a technique known as skinning, which is similar to themes in Windows XP. This article shows how you can apply skinning to Web sites, wrapping their functionality in a new user interface. The technique uses the rich XML classes in the .NET Framework and the built-in extensibility of ASP.NET.

Contents

Extending ASP.NET
ASP.NET Pipeline
Skinning Your Web Pages
Implementing the Handler
FOR XML Queries
Business Objects
HTTP Request Collections
Transform
Sample Application
Conclusion

S kinning refers to the ability to modify the GUI design of an application—wrapping it in another skin—without affecting its functionality. As popular as skinning is in the rich-client world, it has not made much of an impact on the Web client space even though it has a variety of benefits. For example, skinning your Web site can provide customization on a per-user basis. One of the reasons Web skinning has not become as popular as it could be is the lack of tools for creating skinned Web apps.

ASP.NET Web Forms are becoming a popular solution for delivering dynamic Web content. They are simply HTML template pages which are populated with dynamic data at request time. Skinning such pages would require changes to the HTML page template for each different type of page. But changing these page templates in Web Forms requires a great deal of custom code. Rather than trying to force the skinning of ASP pages in this manner, a new tool is needed. Luckily, thanks to the built-in extensibility of ASP.NET, a Web-skinning tool can be built without reinventing the wheel.

Extending ASP.NET

In the original ASP model, the system for handling Web requests was not extensible. Every Web request was mapped to an ASP file that acted essentially as a function that returned a string. If you wanted to produce content in some neutral format and then skin that content for display in the browser, you had to do all the heavy lifting yourself. You had to either manually code the ASP page to build the format-neutral content and then apply the user-selected skin, or you had to build your own ISAPI extension. ISAPI requires you to handle all of the low-level details; you don't have convenient abstractions like the ASP Request and Response objects. You have to build all of that yourself.

When setting out to build ASP.NET, it became obvious that at least two models for servicing Web requests would be needed: Web Forms and XML Web Services. Because support for more than one model was needed, it made sense to abstract the common functionality into a foundation or framework that the concrete models can build upon. This is exactly what the developers of ASP.NET did.

You can see this abstraction in the design of the Web-related namespaces. The foundation is contained within the System.Web namespace. Within this namespace are classes like HttpRequest and HttpResponse. Built upon this foundation are the System.Web.UI and System.Web.Services namespaces. These contain the classes related to Web Forms and XML Web Services, respectively. In this article, I will build a Web request model that is separate from Web Forms and XML Web Services, but that builds upon the same ASP.NET foundation that those models use.

ASP.NET Pipeline

Before extending the ASP.NET model, you need to understand how ASP.NET handles Web requests. The first step in the pipeline is Microsoft® Internet Information Services (IIS), through which all HTTP requests are routed. When you install ASP.NET, it installs an ISAPI extension that forwards requests for ASP.NET file types to the ASP.NET runtime. This forwarding is based on the file extension of the requested file.

Note that this changes slightly in Windows Server 2003. In that product, the HTTP listener code has been moved into the OS kernel and can be used by any process. Thus, services that handle Web requests directly (like ASP.NET) no longer need to route those requests through the IIS process. However, this has little if any impact on the code in this article and more impact on the way the server is configured to handle these Web requests.

Once the Web request is forwarded to the ASP.NET service, it enters the ASP.NET pipeline and a series of events are raised. Some of these events, such as AuthenticateRequest and AuthorizeRequest, are serviced by classes known as HTTP modules (which are similar to ISAPI filters). These classes register their interest in specific requests at Web application startup and perform tasks such as determining user identity and verifying whether the identified user has permission to access the requested resource. However, these modules do not generate the actual content that is returned to the client. That function is performed by an HTTP handler.

HTTP handlers are classes that must implement the IHttpHandler interface. This interface has two members: the ProcessRequest method and the read-only IsReusable property. ProcessRequest is the important one; it is called by the ASP.NET pipeline to generate the actual response content. IsReusable is only used for handler pooling, allowing the runtime to determine if this handler instance can be used again or if it must be disposed of and a new instance created for the next request. ProcessRequest takes one parameter, an HttpContext class which acts as a container for the global intrinsic objects of classic ASP: Request, Response, Server, Session, and Application. It also provides new intrinsic objects such as the User, Cache, and Trace properties. When called, the ProcessRequest function dynamically generates the response content and writes it out to the context.Response.Output stream.

HTTP handler information is stored in the Web.config file. Every Web application has a Web.config file that contains all the configuration information for that application. This information is inherited, so that base configuration can be specified once in the machine.config file rather than repeated in every Web.config file on the server. The data includes the httpHandlers information that maps specific paths and file names (with wildcards) to specific handler classes that will service those requests. Look in the machine.config file, in the httpHandlers section, for the following:

<add verb="*" path="trace.axd" type="System.Web.Handlers.TraceHandler"/>

This specifies that any Web request for "trace.axd" will be handled by the class System.Web.Handlers.TraceHandler. I will be putting a similar entry in the Web.config file of my ASP.NET application to map requests for *.xml files to the custom HTTP handler.

While the ability to plug in a custom handler to service Web requests for custom file types is pretty powerful, ASP.NET gives you an even better mechanism for custom handling of Web requests. ASP.NET also supports handler factories. This is an implementation of the well-known abstract Factory pattern for determining which handler will service a Web request.

In Web.config (or machine.config), you can map a given path to a handler factory rather than to a handler class. You can see this in machine.config:

<add verb="*" path="*.aspx" type="System.Web.UI.PageHandlerFactory"/>

At Web request time, the ASP.NET pipeline will call to the handler factory's GetHandler method. The factory can then use any mechanism it chooses to build a handler class instance, including dynamically generating a handler class the first time a given page is requested. In fact, this is exactly how ASP.NET Web Forms support compiled Web pages. ASPX files are mapped to the PageHandlerFactory class that compiles the given Web page into an executable class the first time the page is requested. After that, PageHandlerFactory retrieves the compiled executable from the server cache, creates a new instance, and returns that handler to be executed for the current Web request.

Skinning Your Web Pages

In order to support Web site skinning, the content for the pages must be generated in a UI-neutral format. Once the UI-neutral content has been generated, a skin must be applied to provide the preferred design to the content. The obvious choice for a UI-neutral format is XML, which implies XSLT as the skin formatter. Building solutions with industry standards like XML and XSLT allows you to concentrate more on the specific implementation of Web skinning. It also allows you to utilize the deep support for XML and XSLT that is provided in the Microsoft .NET Framework Base Class Library.

As a quick aside, SQL Server™ 2000 provides a template mechanism for dynamically generating XML and applying XSLT. This mechanism provides a basic starting point for the Web skin engine, yet SQL Server 2000 XML templates have a couple of limitations for Web skinning. First, XML templates can only retrieve data from a single SQL Server 2000 database. Second, it provides no mechanism for retrieving XML data from other potential sources, such as business objects. Finally, there is no dynamic mechanism for choosing an XSLT skin at run time. The name of the XSLT file must either be embedded statically in the template or passed in on the query string. The Web skin engine I've built in this article overcomes these limitations.

Like the SQL XML template mechanism, the Web skinning engine defines a number of special tags that represent dynamic content to be pulled in at run time. These special tags are defined as part of the "urn:schemas-DevHawk-net:webskinui" namespace, which is mapped by convention to the "skin" namespace prefix. These tags allow the engine to find content exposed by SQL Server 2000 FOR XML queries, business objects, and intrinsic HTTP request data collections (query string, forms, cookies, and so on). The list of these tags is provided in Figure 1. Additionally, this list could be extended to support other sources of content such as XML Web Services and SQL Server 2000 XPath queries.

Figure 1 WebSkin Dynamic Content XML Tags

Tag Description
Class Creates an instance of a business object and gives it a variable name.
Methodcall Invokes a method on a business object that has been declared via the class tag. Stores the result of the method call in the DOM.
Database Creates a connection to a SQL Server 2000 database and gives it a specific variable name.
Query Invokes a FOR XML query against a SQL Server 2000 database that has been declared via the database tag. Stores the result of the query in the DOM.
Parameter Specifies a parameter for a query. Parameter value may be explicitly specified or it may come from one of the HTTP request collections.
Requestvar Retrieves data from one of the intrinsic HTTP request collections, encodes it as XML, and stores it in the DOM.
Transform Invokes a method on a business object that has been declared via the class tag. Method returns a string containing the name of the XSLT file to be applied to the content being returned to the client.

Any XML nodes that aren't in the Web skin schema namespace are left as is. This allows the Web developer to enforce some static structure on the document, similar to the way an ASP developer can simply include static HTML tags in a Web page to be echoed out to the client.

Implementing the Handler

In the handler project file, there are two classes: SkinHandler and SkinUtil. SkinHandler contains all of the required code to be an ASP.NET handler by implementing the IHttpHandler interface and exposing the ProcessRequest method. However, some of the helper functions are located in the SkinUtil class as static methods. I did this because I may build a SkinHandlerFactory at a later time, and the SkinUtil helper functions could then be reused in the dynamically generated SkinHandlers.

The primary function of the SkinHandler class is the ProcessRequest method. The full class is included in the download (see the link at the top of this article), but I will examine several chunks of the code.

For debugging purposes, the first thing ProcessRequest does is check for the __admin query string value. If __admin equals "showxml", ProcessRequest will simply echo the raw XML template to the browser without processing it. This is enclosed within an #if DEBUG...#endif preprocessing block since I only want this feature to be enabled during debugging.

After checking for the debug scenario, the ProcessRequest function loads a DOM instance with the XML file being requested. It then queries the DOM through the GetElementsByTagName method to retrieve all the nodes in the document in the WebSkin namespace. It's important to note that this query returns a dynamic XmlNodeList—when you make changes to the underlying DOM, the XmlNodeList changes too. I will be stepping through the list and replacing the WebSkin nodes with the specified dynamic content. As you can see in the following code snippet, you're not iterating through the XmlNodeList as much as you are repeatedly processing and deleting the first element of the list until the list is empty:

//get a list of all the nodes in the SkinUI Namespace XmlNodeList l = dom.GetElementsByTagName("*", PageUtil.SkinUINamespace); //process each of the SkinUI Nodes. while (l.Count > 0) { XmlNode node = l[0]; ••• //process and replace node }

Once you've got the list of nodes to be processed, a switch statement checks the node's local name against the list of WebSkin nodes listed in Figure 1. Each node type, except for parameter and transform, is processed by its own helper function. Parameter nodes are always children of query nodes, so they get handled by the query helper function. Transforming the XML happens after all the other nodes have been processed, so the transform case handler just stores the node data in a helper class. Since there can only be one transform applied to the XML document, the transform case handler also ensures that the transform node is the only transform node in the document, throwing an error if the user specifies two transform tags.

FOR XML Queries

There are three WebSkin nodes that deal with SQL Server 2000 queries: they are database, query, and parameter. First, the database node sets up a connection to the SQL database via a connection string. The database node also sets up a variable name that will be used to refer back to this database connection. When the handler processes a database node, it creates a SqlConnection instance using the specified connection string. Then it stores the connection instance in an object dictionary that is initialized at the start of the process request function. This object dictionary stores all of the database connections and business object instances that are declared via class and database nodes. ProcessRequest uses a try...catch...finally block to trap errors. In the finally block, the object dictionary is walked and any database connections are explicitly closed.

Once a database has been declared, queries can be defined using the query node. This node specifies the database connection that the query will be run against and the type of query being specified (text or stored procedure, for example). The text of the node is the query to be run and it must be a query or stored procedure that returns XML. Under the query node are zero or more parameter nodes. Here is a query node with a child parameter node that comes from the sample application:

<skin:query var="nwind" type="Text"> <skin:parameter name="@prodID" datatype="varchar" size="30" key="id" collection="querystring" /> SELECT * FROM Products productinfo WHERE productID = @prodID FOR XML AUTO </skin:query>

In this query node, the parameter @prodID is being passed in as the id element of the query string. The parameter can specify any of the request collections and it can also include a default value that is to be used if the specified request collection item does not exist.

The code that processes this node creates a SqlCommand object with the specified SqlConnection and command text. The code then iterates through the child nodes looking for parameter nodes. Once found, the parameter nodes are converted into parameter objects and added to the SqlCommand's parameter list. Once they are processed, the command is executed through the ExecuteXmlReader command by the Execute helper function from the SkinUtil class. The resulting XmlReader is then imported as a document fragment which, in turn, replaces the query node. The XmlDocument class has a very useful function called ReadNode that converts from the stream-based XmlReader to the DOM-based XmlNode representation. The meat of the Execute helper function looks like this:

XmlReader xr = cmd.ExecuteXmlReader(); XmlDocumentFragment xdf = dom.CreateDocumentFragment(); XmlNode xn; while ((xn = dom.ReadNode(xr)) != null) { xdf.AppendChild(xn); }

Business Objects

There are two WebSkin nodes for accessing business objects: class and methodcall. The class node provides an operation similar to that of the database node. It specifies the assembly, class name, and variable name for the business object. When the handler processes a class node, it dynamically loads the listed assembly from the bin subdirectory of the Web skin application. It then creates an instance of the specified class and stores that instance in the same object dictionary that stores database connections. This means that variable names must not be reused between database connections and business objects. This code dynamically loads a class by name:

string filename = context.Request.PhysicalApplicationPath + @"bin\" + GetAttribException(node, "assembly") + ".dll"; Assembly a = Assembly.LoadFrom(filename); Object o = a.CreateInstance(GetAttribException(node, "class"), true); objDict.Add(GetAttribException(node, "var"), o);

Note that the GetAttribException helper function retrieves a named attribute from an XML node. If that attribute doesn't exist, an exception is thrown. There is also a GetAttribNull helper function that returns null when the attribute doesn't exist for situations in which the attribute is optional.

The handler uses reflection to process the methodcall node which provides the variable name and the method name to be invoked. .NET reflection provides a mechanism to query an object to see if it supports a method with a given name. The following code will query and invoke a method via reflection:

Type t = o.GetType(); MethodInfo m = t.GetMethod(GetAttribException(node, "method")); XmlNode newnode = (XmlNode)m.Invoke(o, null);

The important thing to note is that the specified method must conform to a specific signature. Methods invoked through WebSkin must take no parameters and return an XmlNode object (though this could be null). Within the business object, information about the current HTTP context is available through the HttpContext.Current static property.

By requiring a specific method signature, the WebSkin engine is unable to call arbitrary methods of arbitrary business objects. This implies that business objects must be explicitly developed for use in a WebSkin site and not reused. It would be possible to build a parameter mechanism similar to the database parameter support I've just described. However, due to the inability of HTTP request collections to store complex types, this feature would be of marginal use. SQL queries and stored procedures use simple types as parameters and return XML. Many business objects use complex types, such as structs, as either input or output parameters which are difficult to process without code. In order to access business objects that don't have a conforming signature, the user should build a WebSkin-specific business object to handle this arbitrary business object.

HTTP Request Collections

The last sources of content are the various HTTP request collections: QueryString, Form, Cookies, ServerVariables, and Params. The WebSkin node for accessing one of these collections is requestvar. The requestvar node specifies which collection to retrieve content from and optionally specifies the variable name and target namespace. If the variable name is not specified, the entire collection will be included in the DOM. The target namespace enables the generated XML to have a specific namespace. If it is not specified, the parent node's namespace is used instead.

With the exception of the cookie collection, ASP.NET exposes the HTTP request collections as NameValueCollections. Since these collections are implemented the same way and expose their data in the same way, you can use a common method to process most requestvar nodes. The NameValCol method on the handler wraps a call to the SkinUtil.NameValCol method. That method uses a helper function (SkinUtil.NameValColWorkhorse) that encodes a specific collection entry in XML. The NameValCol method calls the helper function one time if the variable name is specified or one time per collection entry if the name is not specified.

ASP.NET exposes the cookie collection as an HttpCookieCollection which, as you can guess, exposes a collection of HttpCookie objects. In addition to the value, HttpCookies have a domain, expiration time, path, and secure setting, so the SkinUtil.CookieCol and CookieCol Workhorse functions work almost identically to the NameValCol and NameValColWorkhorse methods. The only difference is the collection type and encoding mechanism for the collection items. It would have been nice to use one set of methods for this, but the NameValueCollection and the HttpCookieCollection objects have no parent classes in common (except for System.Object).

Transform

Once all the WebSkin nodes have been processed, you need to apply an XSLT skin before sending the content to the browser. The transform tag can specify the XSLT either statically or dynamically. If the transform tag specifies a default attribute, that XSLT file is statically chosen to skin the XML content. Alternatively, if the transform tag specifies a variable and method name, the handler calls the specified method of the variable to determine the XSLT skin file to use. This is very similar to the way method call tags are processed, except that the return value of the transform tag method is a string rather than an XML node.

Once the XSLT skin file has been chosen, the handler calls to SkinUtil.Transform to perform the transform proper. If the XSLT file chosen is null or an empty string, the raw XML is dumped out to the browser client. Otherwise, a new XslTransform object is created, loaded with the selected XSLT file, and then applied to the XML DOM that contains all the content. In order to optimize the loading of the XSLT file, once it's been loaded into the XslTransform object, it's placed in the HTTP context cache. Subsequent requests for this XSLT file do not need to be loaded from disk. In order to keep the cache consistent, a CacheDependency object is created for the cache entry. This dependency automatically removes the cached object if the XSLT file on disk is modified. The XslTransform object is loaded and cached like this:

XslTransform xslt = (XslTransform)(context.Cache[xsltFile]); if (xslt == null) { xslt = new XslTransform(); xslt.Load(xsltFile); context.Cache.Insert(xsltFile, xslt, new CacheDependency(xsltFile)); } xslt.Transform(dom.DocumentElement, null, context.Response.Output);

Sample Application

Included in the code download for this article are both the WebSkin HttpHandler and a sample skinned Web application called DevHawk Books. This sample displays book, author, and publisher information from the pubs sample database. In order to run the sample, a copy of SQL Server or the Microsoft SQL Server 2000 Desktop Engine (MSDE 2000) is required. Each WebSkin page has a database tag that connects to the pubs database on localhost using a trusted connection. If this is not where your database is installed, you will need to modify the connection strings in the database tags on each page.

Figure 2 shows an XSLT skin with simple black text on white background, and Figure 3 shows a more pleasing XSLT skin with a cool logo. Switching between the two is done using a method call to the SetSkin method of the Controller object. This checks the query string for a new skin to use and writes a cookie. The same object also exposes a GetTransform method for retrieving the currently selected skin from the cookie.

Figure 3 XSLT Skin in Color

Figure 2 Black and White XSLT Skin

All the WebSkin pages in the DevHawk Books sample use .xml as their extension. Since IIS is not preconfigured to pass HTTP requests for .xml files along to ASP.NET, I have included a simple VBScript file to perform this configuration. I have also included a VBScript that will configure the DevHawk Books subdirectory as a virtual Web folder. Running the included install.bat file will launch both of these VBScripts.

Conclusion

Using ASP.NET is kind of like having your mind read. If you ever look at a site and think "I need something different," you'll most likely find that the ASP.NET architects have considered that need and provided a mechanism for you to hook in your custom functionality. In this case, I've bypassed the built-in Web Forms and Web Services support to build an entire engine that services Web requests in a unique way.

You can also improve the WebSkin engine's scalability by building a mechanism to compile the skinned Web pages. Writing code that writes code is always tricky, but the Microsoft .NET Framework does most of the heavy lifting through the objects in the CodeDOM namespace.

For related articles see:
HTTP Pipelines: Securely Implement Request Processing, Filtering, and Content Redirection with HTTP Pipelines in ASP.NET
Wicked Code: Code Your Way to ASP.NET Excellence

For background information see:
IHttpHandler Interface
HTTP Handlers

Harry Piersonis a Senior Architect Evangelist with the National Architecture Team at Microsoft. He can be reached through his Web site at https://www.devhawk.net.