Build an XML/XSLT driven Website with .NET
Interested in .NET? Don’t miss SitePoint’s .NET Feature Guide — it’s an excellent resource!
Today, we’ll build a simple dynamic Website using ASP.NET, C#, XML and XSLT. I’ll assume you have prior working knowledge of ASP.NET, C#, XML and XSLT. The site source code provided here shows all details of the implementation, including error handling.
Why XML and XSLT?
There are a couple of different methods by which you can dynamically generate HTML, and they usually allow the writing of the presentation and the business logic to occur together. While writing small sites using these methods is fast, with the growth of the site, the lack of separation makes the maintenance and reuse of existing code difficult.
The XML/XSLT approach naturally separates the presentation from the underlying business logic. There are numerous benefits to this method:
- Better reuse of the business logic code in fast and reliable manner
- The creation of multiple alternative UI, so that serving clients with different capabilities or needs is easy and fast
- User customizations like co-branding, custom look and feel are possible
- Bigger development teams have specialized UI designers and core developers. The separation of the UI reduces the dependencies between the developers, thus improving productivity
- Even the UI unit testing is easier, as an XML document is all that’s needed to reproduce particular tests
But if the UI/business logic separation is the stem of all these benefits, why use XML and XSLT for it?
Think of XSLT as HTML, because most of the code in XSLT is HTML. Given, there couple of rules to observe and the HTML has to be well-formed, but it’s nothing dramatically different. There are free tools to help you convert HTML into XSLT. HTML Tidy, available at the W3C site, is one of them.
What about XML? Well, XML can represent any kind of data structure, and is used as input for the XSLT transformation. Most of the modern databases (SQL Server, Oracle, etc.) can return their query’s result in XML format. In addition, database APIs (ADO) also can convert their results into XML format.
The Site Architecture
The architecture of the site we’ll build is simple. For each HTTP request/response, the business logic and data management layers provide the appropriate data as XML. The presentation layer provides the XSLT and the different parameters for the transformation. The outcome of the XSLT transformation is HTML, which returns to the user.
The reusable presentation layer supports skins and globalization. The XSLT files are stored in a local directory called “UI”. Each “skin” set of XSLT files is stored in a sub directory that bears the skin name. Each globalization set of XSLT files is stored in a subdirectory with the locale id, under the skin directory. The default skin has no name, and the same goes for the globalization set.
Here is how the directory tree looks:
UI
skin1
lcid1
lcid2
skin2
The UI directories contain all the UI files required for the final rendering on the client side, including images, CSS, java script, and more.
The application-independent and reusable functionality is isolated in class UIEngine in the UIEngine.cs file.
The class exposes three properties: ApplicationBase,Skin and Lcid, as well as the methods Transform, ExtractUIParameters and ExtractAction. ApplicationBase contains the relative path of the UI directory, Skin contains the current skin for the request, and Lcid contains the locale of the request.
The Transform method converts the XML data using the specified style sheet, and writes the resulting HTML to the response object.
Transform(HttpContext ctx, XPathDocument xmldoc,
string stylesheet, int lcid, XsltArgumentList arg)
The Web Application Base
By default, the application base is extracted from Web.Config, which is the configuration file for the Web application. This is an XML file that contains the configuration of the Web application. The file contains numerous system settings for the site. There is a section for custom application settings called <appSettings>
. The configuration is accessible through ConfigurationSettings class, part of the System.Configuration namespace. The code is as simple as:
<configuration>
...
<appSettings>
<add key="ui.base" value="ui" />
</appSettings>
</configuration>
m_applicationBase = ConfigurationSettings.AppSettings["ui.base"];
The UI Engine determines the full path to the XSLT file using the application base, skin, locale and XSLT file name:
string path = m_applicationBase;
// check for skin
if(m_skin.Length > 0)
{
path += m_skin + "\";
}
// check for globalization
if(m_locale > 0)
{
path += m_locale.ToString() + "\";
}
// add visualization parameters
arg.AddParam("ui.path","",path);
arg.AddParam("ui.skin","",m_skin);
arg.AddParam("ui.locale","", m_locale.ToString());
// add the stylesheet
path += stylesheet;
To switch skins, we set the name of the skin in the query string:
http://localhost/bookstore/title.aspx?skin=dark
Since HTTP is stateless protocol, the skin name needs to be persisted across different calls. There are a couple of different methods: session variables, query parameters, cookies, and database persistence.
Session variables would be the preferred method if your site was designed to use sessions. Keeping the variables on the server reduces the complexity of the pages and the amount of data transferred back and forth. However, such sites don’t scale as well as sites that don’t use sessions, due to session management overhead.
The query parameters and cookies methods don’t require server sessions, and they scale better, but they also complicate the site design, and will be affected if the user turns off the browser support for cookies. Another issue is security: sensitive data such as credit card numbers shouldn’t be sent as part of query parameters or cookies, as the data won’t be encrypted if secure connections are not used.
Since the sample site doesn’t require sessions and there are few parameters, I choose to pass the state though the URL query’s parameters, that is, skin=dark is augmented automatically to each URL. This is done in the XSL files.
<a href="title.aspx?skin={$ui.skin}&lcid={$ui.locale}">...
A configuration page could be built that showed the available skins or locales. For example:
http://localhost/bookstore/title.aspx?skin=dark&lcid=1026
This URL will display the book list by title using skin “dark” and localized in Bulgarian — 1026. Note that only the UI is localized; the data itself is not. By default, the encoding of the HTML is UTF-8, which works with languages. However some browsers don’t support UTF-8, and locale-specific encoding might be added to the XSL stylesheets.
The visualization parameters are added as parameters to the XSLT transformation. They’re visible in the XSLT and can be used to find HTML elements as CSS and images. For example, we’d use the ui.path parameter to find the CSS file and the image:
<LINK REL="stylesheet" TYPE="text/css" HREF="{$ui.path}css/default.css" />
<img src="{$ui.path}view.gif" alt="view book details" border="0" />
Additional custom parameters for each XSLT file can be added via XsltArgumentList arg, part of the Transform method.
Once the relative path is determined, it is converted to the absolute path and loaded into XslTransform object:
XslTransform xslt = new XslTransform();
xslt.Load(ctx.Server.MapPath(path));
The XML Data is passed as string and loaded into and XPathDocument:
XPathDocument mydata = new XPathDocument(new
XmlTextReader(xmldoc,XmlNodeType.Document,null));
The transformation is performed and the result is written to a string:
StringWriter sw = new StringWriter();
xslt.Transform(mydata,arg,sw);
The string is written to the Response object with buffering turned on:
ctx.Response.BufferOutput = true;
ctx.Response.Write(sw.ToString());
ctx.Response.End();
Create the Web Pages
With the UIEngine done, next is the creation of the Web pages. There are at least two different methods we could use to accomplish this task. The defaut is to create Web Form aspx page and handle the rendering in the Load event. The new class is derived from System.Web.UI.Page and the Load event is handled by a method called Page_Load
. In the source code you’ll find an example of such a page, author.aspx, although it is disabled and not used.
The method that I use is to implement IHttpHandler
. Although I have to write it by hand (no wizard is currently available), the code is simple and it doesn’t require multiple source files. In addition, the compiled application is a single DLL: i.e. there are no page (aspx) files.
IHttpHandler
interface as part ofSystem.Web
namespace and lets you define custom handling of HTTP requests. The interface has one propertyIsReusable
, and one methodProcessRequest
. TheIsReusable
property is set to "true" to allow the object to be reused instead of being created every time is needed. To support session state, IRequiresSessionState has to be implemented as well.
How one would call this handler from a browser? You’ll need to map this handler to the incoming URLs. This is done in the Web.Config file:
<system.web>
...
<httpHandlers>
<add verb="*" path="*.aspx"
type="bookstore.UIHandler,bookstore" />
</httpHandlers>
</system.web>
The ‘verb’ attribute refers to the HTTP verb: GET POST, etc. Setting the value to ‘*
‘ allows any verb to be used. The ‘type’ attribute specifies the handler’s class name, complete with namespace, and the second part is the assembly name. Using the .Net wizard, set the assembly name to the name of project — “bookstore” in this case.
The ‘path’ attribute is more interesting. One can specified a specific URL such book.aspx, wild card name (*.aspx
) or even a path — books/*.aspx
. Using the Website name, the absolute URL in the three cases will look like this:
http://localhost/bookstore/book.aspx
http://localhost/bookstore/<anyname>.aspx
http://localhost/bookstore/books/<anyname>.aspx
Note that in the third case, the server path is considered to be bookstore/books and HttpContext.Server.MapPath would map the path accordingly.
The extension aspx is used only because it is already registered with the Web Server. A custom extension can be used, but the Web Server needs to know that ASP.NET will handle such extensions.
The API
The Website has a simple API. There are four resources (or virtual pages) or actions once the extensions are stripped:
author.aspx
— displays list of the books sorted by authortitle.aspx
— displays list of the books sorted by titlegenre.aspx
— displays list of the books sorted by genrebook.aspx
— display full details for the selected book
Each of these URLs takes the UI parameters ‘skin
‘ and ‘lcid
‘, which are optional. The book.aspx also requires ‘id’ parameter with the id of the book.
The request is handled in method ProcessRequest
. First, the UI parameters are extracted and the requested action is determined:
eng.ExtractUIParameters(ctx);
string action = eng.ExtractAction(ctx);
Then for each action, the XSLT is determined and the XML data is extracted from the business layer logic. In this sample all the actions use the same XML data, which is implemented in class CBookData
.
CBookData bd = new CBookData();
mydata = bd.GetData(ctx);
The action ‘book’ takes addition parameter id is extracted and added as parameter to the XSLT transformation. The other cases, such parameters, could be passed to the business logic layer as well.
string id = ctx.Request.QueryString["id"];
Finally the transformation is performed and the HTML page rendered in the user’s browser.
eng.Transform(ctx,mydata,stylesheet,arg);
Errors are handled by UIEngine.DisplayError
method, which displays all the caught exception’s messages in canned HTML.
catch(Exception err)
{
UIEngine.UIEngine.DisplayError(ctx,err.Message);
}
The error pages can be implemented using a XSLT stylesheets in the same fashion as the main UI.
Source Code and Installation Instructions
The source code is zipped in a file code.zip, which you can download here. There are two C# files UIEngine.cs and UIHandler.cs. The UI is under directory ‘UI’ and contains two skins — default and ‘dark’. The ‘dark’ skin is also localized for locale 1026. The data file ‘book.xml’ is in subdirectory data.
- Using VC.NET create new project using the wizard “Visual C# Projects| ASP.NET Web Application”. As the name of your project enter “bookstore”. You can choose a different name, but you’ll need to modify the namespace in UIHandler.cs and the httpHandlers in Web.Config to reflect that name.
<configuration>
...
<system.web>
...
<httpHandlers>
<add verb="*" path="*.aspx" type="bookstore.UIHandler,bookstore" />
</httpHandlers>
</system.web>
<appSettings>
<add key="ui.base" value="ui" />
</appSettings>
</configuration>
This simple Website harvests the ASP.NET functionality, the entire code is less then 300 lines long, without the UI. The code is readily reusable and the power of XML/XSLT will help you create and maintain dynamic Websites with little effort. For a real site, a production strength database needs to be added. That means designing database schema and database queries, which are very important in ensuring good performance and the scalabilty of the site. Then one would need a module to communicate with the database. ADO.NET is a good choice as an underlying database library. It can handle various databases, it is natively implemented in .NET and it can convert query results in XML format.
Good luck with your site!
This article is part of SitePoint’s .NET Feature Guide — an excellent resource for aspiring and experienced .NET developers. Don’t miss it!