Refactoring XSLT

I guess a quick introduction wouldn't be completely out of order for a first
post. My name is Don Smith and I've been a Dev Consultant at
Microsoft in Charlotte, NC for just over 5 years now. I spend most of
my time helping our customers to be successful with Microsoft's XML
technologies (XSLT, XSD, etc.), Web Services, and distributed architectures.
I've been blogging for several years, but I'm just now deciding to join the MSDN
blog party. Would love to hear from you if you feel so moved. :-)

Refactoring XSLT is a concept that originally dawned on me while delivering
an XML development workshop earlier this year. Here's a touch of background.
According to Martin Fowler, Refactoring is improving the
design of existing code
. With that in mind, one of the most common uses of
XSLT is to transform XML data into HTML. Many enterprise web sites are divided
[roughly] into 3 sections: the header, the main content, and the footer. If
you've even seen or developed an XSLT stylesheet that results in HTML and has a
considerable amount of code in the header and footer sections (or perhaps a
non-trivial navigation system) you know the spaggetti code (mixture of code and
HTML) required to get there is remnicient of Classic ASP. I'm not proposing
there is a way to completely avoid the XSLT/HTML coupling, but this method
can trim up your XSLT code and apply some structure to your solution.

The principles behind this approach are based on stylesheet extensibility
(<xsl:include />) and factoring out large parts of the HTML into their own
files. To illustrate this concept I'll use the following files:

  • Data.xml - A list of our fund raising volunteers and the amount of money
    they raised.
  • Content.xslt - The main stylesheet. It includes Common.xslt, Header.xslt,
    and Footer.xslt.
  • Common.xslt - A stylesheet that contains a script and template callable
    from other stylesheets.
  • Header.xslt - The stylesheet responsible for outputting the header HTML.
  • Footer.xslt - The stylesheet responsible for outputting the footer HTML.
  • Header.txt - A text file containing the HTML for the header section.
  • Footer.txt - A text file containing the HTML for the footer section.
  • Config.xml - A file that contains the locations of the header and footer
    files.

Below are enough files to get the idea of how this approach
works ...

Data.xml - nothing special here; just a
simple XML document

 <?xml version="1.0" encoding="utf-8" ?> 
<contributors xmlns="urn:fund-raiser">
  <volunteer>
    <name>Joe Blow</name>
    <amount>58.25</amount>
  </volunteer>
  <volunteer>
    <name>John Doe</name>
    <amount>81.50</amount>
  </volunteer>
  <volunteer>
    <name>Jane Blow</name>
    <amount>89.75</amount>
  </volunteer>
  <volunteer>
    <name>Julie Doe</name>
    <amount>96.00</amount>
  </volunteer>
</contributors>

Content.xslt - as you can
see, this file's sole function is to produce the main conent for the page (the
table). Its call to the Header template will resolve to the Header.xslt include
and likewise for the footer. The Common.xslt isn't used by anything in this
file. Rather, it will be used by the header and footer (or any other child)
stylesheets.

 <?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:v="urn:fund-raiser"
  xmlns:xsl="https://www.w3.org/1999/XSL/Transform">
  
  <xsl:include href="Common.xslt" />
  <xsl:include href="Header.xslt" />
  <xsl:include href="Footer.xslt" />

  <xsl:template match="/">
    <xsl:call-template name="Header" />
    <table width="250">
      <xsl:apply-templates select="v:contributors/v:volunteer"/>
    </table>
    <xsl:call-template name="Footer" />
  </xsl:template>

  <xsl:template match="v:volunteer">
    <tr>
      <td width="50%"><xsl:value-of select="v:name" /></td>
      <td width="50%"><xsl:value-of select="v:amount" /></td>
    </tr>
  </xsl:template>
  
</xsl:stylesheet>

Header.xslt - all of the
header HTML data is in an external file defined in Config.xml. The path is
retrieved and passed into the FileContents template as a parameter. The
FileContents template will resolve to Common.xslt.

 <?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" 
  xmlns:xsl="https://www.w3.org/1999/XSL/Transform">

<xsl:template name="Header">

  <xsl:variable name="headerfile" 
    select="document('Config.xml')/configuration/header" />

  <xsl:call-template name="FileContents">
    <xsl:with-param name="filepath" select="$headerfile" />
  </xsl:call-template>

</xsl:template>

</xsl:stylesheet>

Common.xslt - this is the
only part of the solution I'm not crazy about: the script. Granted, even if
you're not using .NET, most popular XSLT implementations have similar
extension facilities. As you can see below, the FileContents template calls the
GetContents function to retrieve the contents of the header. The XPath
document() function can't be used here because the header and footer files
don't contain well-formed markup.

 <?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"

  xmlns:fac="urn:factoring-xslt"
  xmlns:msx="urn:schemas-microsoft-com:xslt"
  xmlns:xsl="https://www.w3.org/1999/XSL/Transform">
  
  <msx:script language="c#" implements-prefix="fac">

    public string GetContents( string filepath )
    {
      try
      {
        using( System.IO.TextReader reader = 
        System.IO.File.OpenText( filepath ) )
        {
          return reader.ReadToEnd();
        }
      }
      catch( System.Exception ex )
      {
        return ex.Message;
      }
    }

  </msx:script>

  <xsl:template name="FileContents">

    <xsl:param name="filepath" />
    <xsl:value-of select="fac:GetContents( $filepath )" 
      disable-output-escaping="yes" />

  </xsl:template>

</xsl:stylesheet>

If you want to play around with these files, feel free to download my
sample solution here
. I've never seen this approach used anywhere and
I've never seen anyone else mention it. I thought a purely original post would
be a good start on MSDN. So, if you decide to implement any of these ideas,
please test thoroughly. I can't stress that enough. Here are
some other resources you might find useful:

If you have any thoughts about this stuff, I'd love to hear 'em. Happy
coding!