A Practical Comparison of XSLT and ASP.NET

 

Chris Lovett
Microsoft Corporation

February 19, 2001

Download Xml02192001.exe.

Introduction

People are using XML to manage the publishing process for large, complex Web sites. Examples include an interview with Mike Moore on how www.microsoft.com has used XML to manage its complex needs and Streamlining Your Web Site Using XML, a high-level overview of how companies such as Dell use XML to streamline their entire publishing process.

The questions I am getting from a lot of customers are: Should they dump XML/XSL and go write a bunch of C# ASP.NET code instead? If they have already heavily invested in XSLT, does ASP.NET provides any value to them? Is there some middle ground where they can get the best of both worlds? If so, what are the strengths and weaknesses of each technology and when should people use them?

I will drill in on a specific example so I can compare and contrast XSLT versus ASP.NET. The example is the MSDN TOC. MSDN found that XML was ideal for managing its large table of contents (TOC). The contents of this TOC come from hundreds of groups around the company. The XML format provided a way to glue together disparate back-end processes that would have been much harder to change. XML/XSL also made it possible to reach different browsers on different platforms.

Given that developers are finding that XML is the best way to manage the back-end data that goes into a Web site, let's take a look at how you take this XML data and turn it into HTML. First I will look at the ASP.NET solution.

The ASP.NET Solution

A friend of mine, Dan Wahlin, wrote one of the best XML-based Menu controls for ASP.NET that I've seen, Make Web Browsing Easier. (You will need a DevX membership to access this article.) With Dan's permission, I've enhanced his code a bit and wrapped it in a WebControl.

Figure 1. XML-based menu wrapped in a WebControl

The WebControl is then embedded in an ASP.NET page in a reusable way:

<XmlMenu:XmlMenuBar id="MyMenus" runat="Server" src="menus.xml">
</XmlMenu:XmlMenuBar>

Now let's look at the implementation of this control. The logic to render the HTML menu starts with overriding the virtual WebControl Render() method.

    protected override void Render(HtmlTextWriter output) {
        XmlDocument doc = (XmlDocument)Context.Cache["Master"];
        if (doc == null)
        {
          string filepath = Context.Server.MapPath(_src);
          doc = new XmlDocument();
          doc.Load(filepath);
          ResolveSubMenus(doc);
          Context.Cache.Insert("Master", doc, 
              new CacheDependency(filepath));
        }
        RenderMenuBar(doc, output);
    }

The general algorithm here is to load the menu.xml file and resolve any external references within this file, then cache the master menu document in memory so that subsequent requests are faster. This ability to cache is an important feature of ASP.NET.

Resolving the external submenus is done by walking the document using XPath, looking for src attributes, loading the submenu documents, and merging them into the master document:

    void ResolveSubMenus(XmlDocument doc)
    {
        DocumentNavigator nav = new DocumentNavigator(doc);
        nav.Select("//*[@src]"); // XPath for finding all src attributes.
        while (nav.MoveToNextSelected())
        {
            string src = nav.GetAttribute("src");
            XmlDocument submenudoc = new XmlDocument();
            submenudoc.Load(Context.Server.MapPath(src));
            DocumentNavigator nav2 = new DocumentNavigator(submenudoc);
            nav2.MoveToDocumentElement();
            nav.MoveChildren(TreePosition.LastChild, nav2);
        }
    }

Note: The ability to pre-compile XPath expressions is an important feature of the new XML classes in the .NET Framework. This gives us added performance.

Next we take this fully resolved master XmlDocument and write the HTML output to the HtmlTextWriter that the ASP.NET Framework passed in. The top-level menu bar is written out as an HTML table using pre-defined cascading style sheet (CSS) classes so that the overall look of the menu can be controlled via a separate .css file.

There are two steps here: writing out of the top-level menu bar items, then writing out of the menus themselves. These menus are written out in hidden DIV elements, which some client-side ECMAScript code pops up dynamically.

    object _linkexpr; // pre-compiled XPath expressions.
    object _nameexpr;

    void RenderMenuBar(XmlDocument doc, HtmlTextWriter output)
    {
        output.Write("<table width='100%' class=clsMenuBar border='0' ");
        output.Write("cellspacing='0' cellpadding='0'>\n<tr><td>\n");
        output.Write("<table class=clsMenuBar border=0 cellspacing=0 ");
        output.Write("cellpadding='0'>\n");
        output.Write("<tr align='left' valign='middle'>\n<td>&nbsp;&nbsp;</td>\n");

        DocumentNavigator nav = new DocumentNavigator(doc);
        _linkexpr = nav.Compile("link");
        _nameexpr = nav.Compile("name");

        nav.MoveToDocumentElement();
        RenderMenuBarItems(nav, output);
        output.Write("</tr></table>\n</td></tr></table>\n");   
        nav.MoveToDocumentElement();
        RenderMenus(nav, output);
    }

Render the menu bar items by walking through the children of the root element generating table cells for the menu bar and special customizable menu bar spacer cells. It also generates the DHTML for interacting with the mouse, so that when the mouse moves over the menu the submenu pops up. Various XPath expressions are used to pull out the name and link parts of each menu found in the root-level menu item. As you can see, the code is fairly bulky. The output calls are done in a pretty verbose way to minimize string concatenation. This yields about a 10 percent perf improvement.

    void RenderMenuBarItems(XmlNavigator nav, HtmlTextWriter output)
    {
        int i = 0;    
        nav.Select("child::*"); // select child elements
        bool first = true;

        if (!nav.MoveToFirstChild()) return;

        do
        {
            if (nav.Name == "menu")
            {
                if (! first) {
                    output.WriteLine("<td class='clsMenuBarCell' nowrap>");
                    output.Write(_barSpacer);
                    output.Write("</td>");
                }
                string name = "menu" + i++;
                output.Write("<td class='clsMenuBarCell' nowrap>");
                output.Write("<a id='start' class='clsMenuBarCell' ");
                output.Write("style='text-decoration: none; cursor: hand'");

                string target = "";
                string href = "";
                if (nav.SelectSingle(_linkexpr)) // is it also a link ?
                {
                    target = nav.GetAttribute("target");
                    href = nav.InnerText;
                    nav.MoveToParent();
                }
                if (HasSubMenu(nav)) // submenu ?
                {
                    output.Write("onmouseover='startIt(\"");
                    output.Write(name);
                    output.Write("\",this,0)' ");
                }
                else if (href != "")
                {
                    output.Write(" onMouseOver=\"stateChange('',this,'');hideDiv(0);\" ");
                    output.Write("onMouseOut=\"stateChange('',this,'');\" ");
                }
                if (href != "") 
                {
                    output.Write("href='");
                    output.Write(href);
                    output.Write("' onClick=\"Select('");
                    output.Write(href);
                    output.Write("','");
                    output.Write(target);
                    output.Write("')\" ");
                }
                output.Write(">");
                nav.SelectSingle(_nameexpr); 
                string text = nav.InnerText;
                output.Write(text);// grab the menu name
                nav.MoveToParent();                        
                first = false;
            }
        } 
        while (nav.MoveToNext());
    }

Rendering the menus into hidden DIV elements is next. Each menu is given a unique ID, which the client-side ECMAScript code uses to tie together the menu items and their related submenus:

    void RenderMenus(XmlNavigator nav, HtmlTextWriter output)
    {
        int i = 0;
        if (!nav.MoveToFirstChild()) return;
        do
        {
            if (nav.Name == "menu")
            {
                string name = "menu" + i++;
                RenderMenu(name, 0, nav, output);
            }
        }
        while (nav.MoveToNext());
        nav.MoveToParent();
    }

RenderMenu does the bulk of the work. It is a recursive algorithm that walks each submenu, writing out either container elements that point to another submenu or leaf elements that are directly selectable. Each menu and menu item has to have a unique ID, which is generated to help the ECMAScript code find items and their related submenus. It is a two-pass algorithm over each submenu, first writing out the elements for that menu, then writing out any submenus it finds.

   void RenderMenu(string thisMenu, int level, XmlNavigator nav, HtmlTextWriter output)
    {
        output.Write("<div id='");
        output.Write(thisMenu);
        output.Write("' class='clsMenu'>\n");

        if (nav.MoveToFirstChild())
        {
            int i = 0;
            do
            {
                if (nav.Name == "item")
                {
                    if (HasSubMenu(nav)) // container ?
                    {                        
                        string currentMenu = thisMenu + "_" + (i+1);
                        string text = "";
                        if (nav.SelectSingle(_nameexpr)) {
                            text = nav.InnerText;
                            nav.MoveToParent();
                        }                        
                        string id = thisMenu + "_span" + (i+1);
                        output.Write("<span id=\"");
                        output.Write(id);
                        output.Write("\" class='clsMenuItem' onMouseOver=\"stateChange('");
                        output.Write(currentMenu);
                        output.Write("',this,");                        
                        output.Write(Int32.Format(level+1, "d"));
                        output.Write(")\" onMouseOut=\"stateChange('',this,'')\">");
                        output.Write(s_Image);
                        output.Write(text);
                        output.Write("</span><br>\n");            
                    } 
                    else 
                    {
                        // leaf.                        
                        string href = "";
                        string target = "";
                        if (nav.SelectSingle(_linkexpr)) {
                            href = nav.InnerText;
                            target = nav.GetAttribute("target");
                            nav.MoveToParent();
                        }
                        string text = "";
                        if (nav.SelectSingle(_nameexpr)) {
                            text = nav.InnerText;
                            nav.MoveToParent();
                        }
  
                        output.Write("<span id=\"");
                        output.Write(thisMenu);
                        output.Write("_span");
                        output.Write(Int32.Format(i+1,"d"));
                        output.Write("\" class='clsMenuItem' ");
                        output.Write("onMouseOver=\"stateChange('',this,'');hideDiv(");
                        output.Write(Int32.Format(level+1,"d"));
                        output.Write(")\" onMouseOut=\"stateChange('',this,'')\" onClick=\"Select('");
                        output.Write(href);
                        output.Write("','");
                        output.Write(target);
                        output.Write("')\">");
                        output.Write(text);
                        output.Write("</span><br>\n");
                    }
                }
                i++;
            }
            while (nav.MoveToNext());
            nav.MoveToParent();
        }
        output.Write("</div>\n");

        if (nav.MoveToFirstChild()) // pass2, write out submenus recurrsively.
        {
            int i = 0;
            do
            {
                if (nav.Name == "item")
                {
                    if (HasSubMenu(nav))
                    {
                        string currentMenu = thisMenu + "_" + (i+1);            
                        RenderMenu(currentMenu, level+1, nav, output);
                    } 
                    else 
                    {
                        // leaf.                        
                    }
                }
                i++;
            } while (nav.MoveToNext());
            nav.MoveToParent();
        }
    }

Lastly there is a little helper method we've been using for figuring out whether a menu item has a submenu:

    bool HasSubMenu(XmlNavigator nav) 
    {
        if (nav.GetAttribute("src") != "")
            return true;

        if (!nav.MoveToFirstChild()) return false;       
        do 
        {
            if (nav.Name == "item") {
                nav.MoveToParent();
                return true;
            }
        } while (nav.MoveToNext());
        nav.MoveToParent();        
        return false;
    }

The XSLT Solution

Note: The XSLT style sheet uses xsl:call-template with variables and parameters, so it does not run on .NET Framework Beta 1. It does run under MSXML 3.0.

So, there is a lovely little WebControl in ASP.NET that integrates XSL transformations into the ASP.NET rendering model. You invoke it like this:

<asp:xml 
    id="MyMenus" 
    TransformSource="menubar.xsl"
    DocumentSource="menubar.xml"
    runat="Server">
</asp:xml>

The problem is that we do not have a simple XML file. We need to resolve all the submenus first, and there is no way to do this without writing some C# code. So I had to create a little class called XmlMenuResolver that contains the ResolveSubMenus method from the previous example and wire it up to the asp:xml control:

<%
    XmlDocument doc = XmlMenuResolver.Resolve("menubar.xml", Context);
    MyMenus.Document = doc;    
%>
<asp:xml 
    id="MyMenus" 
    TransformSource="menubar.xsl" 
    runat="Server">
</asp:xml>

The entire HTML rendering done by asp:xml control is then controlled by the menubar.xsl style sheet. The style sheet follows pretty much the same recursive model as the XmlMenuBar control. First there is a top-level template to kick things off:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> 
<xsl:template match="/">
<xsl:apply-templates select="menubar"/>
</xsl:template>
...

The menu bar template walks the top-level menu items and writes out the horizontal HTML <TABLE> of top-level menu bar items. Notice that the xsl:if and xsl:choose elements correspond almost one to one to the logic in the original RenderMenuBarItems method. Notice also that the HTML <TABLE> template is easier to manage when it's embedded in an XSL style sheet than when it is generated by output.Write() calls.

<xsl:template match="menubar">
    <TABLE WIDTH='100%' CLASS="clsMenuBar" BORDER='0' CELLSPACING='0' CELLPADDING='0'>
    <TR><TD>
    <TABLE CLASS="clsMenuBar" BORDER='0' CELLSPACING='0' CELLPADDING='0'>
    <TR ALIGN='left' VALIGN='middle'>
    <TD>&#160;&#160;</TD>
    
    <xsl:for-each select="menu">
        <TD CLASS='clsMenuBarCell' NOWRAP="true">
        <A ID='start' CLASS='clsMenuBarCell' STYLE='text-decoration: none; cursor: hand'>
        <xsl:choose>
        <xsl:when test="item">
            <xsl:attribute name="ONMOUSEOVER">startIt(
               'menu_<xsl:value-of select="position()"/>',this,0)</xsl:attribute>            
        </xsl:when>
        <xsl:otherwise>
            <xsl:attribute name="ONMOUSEOVER">stateChange('',this,'');hideDiv(0);</xsl:attribute>
            <xsl:attribute name="ONMOUSEOUT">stateChange('',this,'')</xsl:attribute>
        </xsl:otherwise>
        </xsl:choose>
        <xsl:if test="link">
            <xsl:attribute name="HREF"><xsl:value-of select="link"/></xsl:attribute>
            <xsl:attribute name="ONCLICK">Select('<xsl:value-of select="link"/>',
               '<xsl:value-of select="link/@target"/>')</xsl:attribute>
        </xsl:if>
        <xsl:value-of select="name"/>
        </A>
        </TD>
    </xsl:for-each>
    </TR></TABLE>
    </TD></TR></TABLE>        
    <!-- write out the hidden divs for subsmenus -->
    <xsl:for-each select="menu">
        <xsl:call-template name="submenu">
            <xsl:with-param name="parent">menu</xsl:with-param>
            <xsl:with-param name="level">0</xsl:with-param>
        </xsl:call-template>
    </xsl:for-each>
</xsl:template>

This calls out to a template called "submenu" that passes some parameters. These parameters are exactly the same recursive menu name and level arguments that you see going into the RenderMenu method. The submenu template does the same thing as RenderMenu, recursively calling itself for submenus:

<xsl:template name="submenu">
    <xsl:param name="parent"/>
    <xsl:param name="level"/>
    <xsl:variable name="name"><xsl:value-of select="$parent"/>_<xsl:value-of select="position()"/></xsl:variable>
  
    <DIV CLASS='clsMenu'>
    <xsl:attribute name='ID'><xsl:value-of select="$name"/></xsl:attribute>
        <xsl:for-each select="item">
            <xsl:choose>
            <xsl:when test="item"> <!-- submenu -->
              <SPAN CLASS='clsMenuItem' ONMOUSEOUT="stateChange('',this,'')">
              <xsl:attribute name="ID"><xsl:value-of select="$name"/>_span<xsl:value-of select="position()"/>))")</xsl:attribute>
                 <xsl:attribute name="ONMOUSEOVER">stateChange(
                     '<xsl:value-of select="$name"/>_<xsl:value-of select="position()"/>',
                     this, '<xsl:value-of select="$level+1"/>');</xsl:attribute>
                <IMG ALIGN="right" VSPACE="2" HEIGHT="10" WIDTH="10" BORDER="0" 
                     SRC="images/right_arrow.gif" NAME='arrow'/>
                <xsl:value-of select="name"/>
                </SPAN><BR/> 
            </xsl:when>
            <xsl:otherwise> <!-- leaf -->
                 <SPAN CLASS='clsMenuItem' onMouseOut="stateChange('',this,'')">
                 <xsl:attribute name="ONMOUSEOVER">stateChange('',this,'');
                     hideDiv(<xsl:value-of select="$level+1"/>)</xsl:attribute>
                 <xsl:attribute name="ID"><xsl:value-of select="$name"/>_span<xsl:value-of select="position()"/></xsl:attribute>
                <xsl:attribute name="ONCLICK">Select('<xsl:value-of select="link"/>',
                   '<xsl:value-of select="link/target"/>')</xsl:attribute>
                <xsl:value-of select="name"/>
                </SPAN><BR/>
            </xsl:otherwise>
            </xsl:choose>
        </xsl:for-each>
    </DIV>
    <!-- recurrse submenus -->
    <xsl:for-each select="item">
        <xsl:if test="item">
            <xsl:call-template name="submenu">
                <xsl:with-param name="parent"><xsl:value-of select="$name"/></xsl:with-param>
                <xsl:with-param name="level"><xsl:value-of select="$level+1"/></xsl:with-param>
            </xsl:call-template>
        </xsl:if>
    </xsl:for-each>
</xsl:template>
</xsl:stylesheet> <!-- this is the end of the style sheet -->

Notice also that the whole thing is more compact than the code in the XmlMenuBar.cs example. But it's also a lot more dense. Quite frankly, it is also a pretty advanced use of XSLT, which would intimidate all but the most advanced XSLT users. It took me a while to get it working. It would have been a lot easier if there were good integrated XSLT debugging environment.

Performance

So which solution performs better? On my machine the XSLT version gets 33 requests per second (using MSXML 3.0). The C# version gets about 120 requests per second. A preliminary test of a .NET Beta 2 version of XslTransform does about 47 requests per second. So clearly the C# code is faster. This is understandable, given that the C# code is hand-optimized XmlNavigator code that minimizes the number of XPath evaluations.

However, XSLT can also be performed on Internet Explorer 5.x clients, although this particular style sheet requires MSXML 3.0, which will not be integrated until a future version of Internet Explorer. When XSLT is offloaded to the client, the server is then just publishing static HTML pages. These HTML pages still have to fetch the XSLT style sheet, but this gets cached on the client side. Internet Information Server 5.0 can do around 1,000 static HTML pages per second on my machine, depending on their size.

Conclusion

ASP.NET Pros ASP.NET Cons
The ability to do custom performance tweaking at every step results in perf win. The embedding of HTML directly inside code is bad. Customers typically treat code development and test as a much more expensive process than tweaking HTML.
Lower barrier to entry since you probably have to learn C# anyway, unless you have a big team that can divide into XSLT expertise and C# expertise. Tight dependency between XmlMenuBar server-side code and DHTML client-side script code is a maintenance problem.
Better debugging tools available today.  
XSLT Pros XSLT Cons
Can offload HTML rendering to rich clients. XSLT gets complex pretty quickly.
Embedded HTML is easier to manage in XSLT—especially when you need to publish your site in multiple languages. You can't live entirely in the XSLT world. You still have to write code like the ResolveSubMenus code above.
XSLT is declaritive and higher level. This means there is more opportunity to optimize the performance of the XSLT version. For example, an XSLT engine that compiles the style sheet down to IL could run quite a lot faster.  
XSLT has cross-platform advantages, given that it is already a W3C recommendation and XSLT processors exist on other platforms.  

Note: I am a programmer, so writing code comes naturally and my opinions may be biased by this fact. Non-programmers may find the XSLT approach much simpler.

There is no clear winner. There are pros and cons to each solution. Developers will have to write application-specific code anyway (such as my XmlMenuResolver class), so I could certainly see the argument for staying in a C#-only environment and saving on the XSLT training costs. On the other hand, if customers have already invested heavily in XSLT, as www.microsoft.com has, and they have clear business value already derived from that, then integrating the XSLT solution into an ASP.NET environment as I have shown here can provide the best of both worlds.