Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#
Article

Multiple Document Output from XSL Transformations using Parameters and C#

Rate me:
Please Sign up or sign in to vote.
3.90/5 (8 votes)
20 Apr 20049 min read 56K   1.2K   21   2
A small C# application to demonstrate the use of parameters in producing a number of static web-pages from a single XML, XSL pair.

XML Transform App

Introduction

So you've been reading up on XML a little. You understand the basics of XML documents, XSL transformations, and maybe you even have a fuzzy sort of "schema awareness". The only catch is that now you are all read up, you're still not really sure what you can do with it. If I am describing you then you might find this article useful. I have written it as a solution to a problem posed by a friend and I am treating it as a useful exercise to find out how I can better utilize XML in my distinctly average programming solutions. There is nothing particularly cutting-edge here....just an attempt to provide a simple XSL example.

I have tried to spell everything out very carefully so that this can be used as a tutorial but I am also aware that in doing so, I am also exposing the shoddy parts of code, so any suggestions for improvement will be greatly welcome. In particular, there is only very blunt error checking present.

Background

The problem is a simple one. You would like to produce a number of very similar web-pages. Each page consists of a title and some links to relevant articles for that title. In addition, each page contains a drop-down box which redirects you to the other available pages.

Example Page Produced

In my friend's problem, he wanted a static page for every book of the Bible, complete with links to relevant articles. Of course, you could just code 66 static web-pages. The problems with this approach are well known. Not only is it dull to produce but you have the problem of ensuring consistent style across each page. If you want to make a change later on, you have to change it on 66 different pages. Let's put our XML knowledge to good use instead.

Multiple Page Output from Transformations

The XML Links File

We start off with a single XML file that will contain the relevant links for every topic. This way, we reduce the 66 files to 1 file. Here is a snippet from this file (XMLLinks.xml in the download).

XML
<?xml version="1.0" encoding="utf-8" ?>
<links>
  <link id="Ruth">
    <block>
      <![CDATA[
      <p style="margin-left: 26">
      <font face="Arial">
      <strong>
        Ruth obeys God and finds Love (Ruth)
      </strong>
      </font> 
      ...
      ]]>
    </block>
    <block>
      <![CDATA[
      <p style="margin-left: 26">
      <font face="Arial">
      <strong>
        There is a Redeemer (Ruth)
      </strong>
      </font>
      download as ...
      </p>
      ]]>
    </block>
  </link>
  <link id="Mark">
    <block>
      <![CDATA[
      ...
      ]]>
    </block>
    ...
  </link>
</links>

Each link element represents a choice in the drop-down box and each block element is a block of HTML that will be copied to the relevant static web-page on processing.

Now, I must immediately defend myself here from the charge of ruining the XML concept by introducing HTML, hidden in CDATA blocks. Ideally, I agree, it would be preferable to have only the link data inside this file and save the formatting for the stylesheet. However, this would have made it harder for my friend to use (due to unfamiliarity with XSL). There are pros and cons with this kind of trick (mostly cons), but if it makes it more accessible for your end-user then it is sometimes a choice you make.

The XSL File

OK, so now we need a stylesheet which will take the XMLLinks.xml file and process it to spit out the 66 static pages. And here comes the rub. If we were using an XSLT engine which supports the use of xsl:document (for outputting more than one document with each transformation), for instance the superb Saxon engine, then we could write a single XSL file and produce our 66 pages from a single transformation. However, we are not, and instead this gives us a chance to show off the use of parameters.

The basic idea behind parameters for stylesheets is this: you define the parameters at the top of the stylesheet, and when you run the transformation, you provide their values. These parameters can then be used in the transformation to determine the action of the stylesheet.

Here is the start of the stylesheet (XMLLinks.xsl in the download):

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

  <xsl:param name="book"/>

  <xsl:output method="html"/>

  <xsl:template match="/">
    <html>
    <head>
    ...

Notice the declaration of the book parameter. We will use the value of this parameter to determine the book of the Bible that we are currently outputting. In other words, we will run the transformation 66 times, each time passing in a different book of the Bible as a parameter.

How is this parameter used then? Let's have a look at another excerpt from the stylesheet:

XML
...
<div style="text-align:left;">
            
  <p>
    <font color="#008000" size="7" face="Arial">
      <b><xsl:value-of select="$book"/></b>
      <br />
    </font>
  </p>

  <p style="font-family: verdana;">Results:</p>
  <xsl:for-each select="/links/link[@id=$book]/block">
    <xsl:value-of disable-output-escaping="yes" select="."/>
  </xsl:for-each>
</div>
...

Here is the time to apologize for the inconsistent use of both old tag based formatting and CSS style formatting. I was copying blocks of code from the original website in question and then inserting bits of my own without refactoring it all. Never a good idea.

So we can see the book parameter used in two places. Firstly, in outputting the large title specifying the book for that page. Also, we copy each block from the link element which has that book as its id. Notice that we have to disable the output escaping so that the HTML (inside CDATA sections) comes out as real HTML and not the character-encoded equivalent.

The drop-down select element at the top of the page is produced using:

XML
<select name="selectBook" onChange="openURL();" size="1" style="width:150px;">
  <option value="index.html">Please Choose...</option>
  <xsl:apply-templates select="/links/link" mode="index"/>
</select>
...
<xsl:template match="link" mode="index">
  <option value="{@id}.html"><xsl:value-of select="@id"/></option>
</xsl:template>

Notice here that we need some client-side script to produce the redirection. When a choice is made in the select element, the onChange code is executed. This code needs to go in the head element of the page. This is achieved by the following in the XSL stylesheet:

JavaScript
<SCRIPT LANGUAGE="JavaScript">
  <xsl:text disable-output-escaping="yes"><![CDATA[<!--]]></xsl:text>
  function openURL()
  {
    selInd = document.theForm.selectBook.selectedIndex;
    goURL = document.theForm.selectBook.options[selInd].value;
    top.location.href = goURL;
  }

  <xsl:text disable-output-escaping="yes"><![CDATA[//-->]]></xsl:text>
</SCRIPT>

Here we see the tweak needed to correctly output the JavaScript hide <!-- and //-->. We use the xsl:text with output escaping disabled to achieve this.

The C# Application

Finally, then we need to write an application which executes a sequence of transformations passing in different parameters for each transformation. We could of course write a quick and dirty app to do it in a few lines, but while we're in programming mode, let's write a slightly more general solution.

Requirements for the program:

  • It should be able to produce a sequence of output pages, not just one.
  • The output pages can either be named sequentially e.g. page1.htm, page2.htm,...
  • ...or named using the parameter from the transformation itself e.g. genesis.htm, exodus.htm,...
  • Each transformation can be provided with any number of parameters.
  • Each parameter can either be a textual value or can specify an XPath expression where the value of the parameter is set to the evaluated inner text of the first node in the input document which satisfies that XPath expression.
  • In addition, if the first parameter specifies an XPath expression valid for the input XML document, then the transformation can be run multiple times, once for each node in the node-set obtained from the XPath expression and the value of the parameter will be set each time to the inner text of the current node (that was a mouthful).
  • Since all the above adds up to quite a lot of settings, you should be able to save the settings and load them in from the file menu. They will be saved in XML format (naturally).

Perhaps at this stage, it is better to run the application and see it working.

Download the first of the links at the top of this page and extract the zip file to c:\temp (stick with this directory initially). Navigate to the folder CodeProjectTransform in c:\temp and inside this folder, execute Transform.exe. From the File menu, choose Load Settings and load the Config.xml file which is in the same directory. This will fill in the settings in the various textboxes. Click on the tab named "Execute" (along the top of the form) and click the Execute button. If you get a successful sort of message, you can close the program down (File->Exit) and navigate to the sub-folder Output. You should now have 3 HTML files there (mark, ruth, luke) which can be opened in your browser.

XML Transform App

I shall leave out the basic 'plumbing' behind the user interface (tab control and menu), and concentrate instead on the code that runs the actual transformation itself. I shall also ignore the loading and saving of the transformation settings because although it does use an XML file to store the settings, the load settings code is crude and the save settings has not yet been implemented.

I should just point out one thing of interest here before I begin. The .NET Framework while complete with a nice OpenFileDialog control, seems to lack a similar ChooseFolderDialog. I needed this because the user must select the folder where the generated files are to be stored.

You need to select the folder to contain the output files

I found a very simple wrap-around class OpenDirectoryDialog via a posting, which does the job nicely.

Choose Directory Dialog

OK, so onto the transformation code run when the Execute button is clicked. If there are no parameters provided or the first parameter does not have the "Process for each XPATH Node" checkbox checked, then it is a simple one off transformation, for which we use the code:

C#
XmlDocument xmlInput = new XmlDocument();
xmlInput.Load(txtInput.Text);

XslTransform xslFormat = new XslTransform();
xslFormat.Load(txtTransform.Text);

...

// simple one-off transformation
XsltArgumentList args = new XsltArgumentList();

foreach(ParameterList.ParametersRow p in dgParamList.Parameters.Rows)
  args.AddParam(p.Name,"",resolveParam(p.Value,p.isXPATH, xmlInput));

string strFilename = txtFolder.Text + @"\" + 
       txtSequential.Text + "." + txtExtension.Text;
StreamWriter writer = new StreamWriter(strFilename);
xslFormat.Transform(xmlInput,args,writer);
writer.Flush();
writer.Close();
txtResults.Text+= "Completed transformation.\r\n";

Let's step through this code carefully. First, we define and initialize two objects: an instance of XmlDocument (need using System.Xml;) to hold our XML data file and an instance of XslTransform (need using System.Xml;) to hold our XSL stylesheet.

Next, we define and initialize args, an instance of XsltArgumentList. This object is used to store all the parameters for a transformation and is itself passed in as a parameter to the Transform method of the XslTransform object.

We need to fill the args object with all the parameters for the transformation so we iterate through every ParametersRow in our DataSet dgParamList adding the parameters name and calling the method resolveParam to determine the corresponding value of the parameter. Notice that I have used a strongly-typed Dataset here which enables me to extract the parameter info from the DataSet using p.Name and p.Value.

C#
private string resolveParam(string pValue, bool isXPATH, XmlDocument doc)
{
  string retValue;
  if(!isXPATH)
    retValue = pValue;
  else
  {
    try
    {
      XmlNode nde = doc.SelectSingleNode(pValue);
      if(nde==null)
        retValue = String.Empty;
      else
        retValue = nde.InnerText;
    }
    catch
    {
      txtResults.Text += "Invalid XPath Query Causing Error\r\n";
      retValue = String.Empty;
    }
  }
  return retValue;
}

This method checks to see if the parameter value is claiming to be an XPath expression. If it is, then it evaluates the expression against the xml input document and returns the text contents of the first node match. If not, then the parameter is simple text and this text is returned.

Finally, returning to the simple transformation code, a StreamWriter object is used to store the results and the transformation is executed. This is important. If we don't use a StreamWriter object (e.g. we tried to store the results of the transformation initially in a XmlReader object), then we would run into problems with the disable-output-escaping attribute in our stylesheet. The transformation would completely ignore this directive (as is its right) because the directive concerns the serializing of an XML file, not an in-memory D.O.M. tree.

That just leaves the more complicated sequence of transformations, where a transformation is executed for every node in a node-set obtained from the XML input.

C#
// more complicated sequence of transformations
try
{
  XmlNodeList nl = xmlInput.SelectNodes(dgParamList.Parameters[0].Value);
        for(int t=0;t<nl.Count;t++)
  {
    try
    {
      XsltArgumentList args = new XsltArgumentList();
      args.AddParam(dgParamList.Parameters[0].Name,"",nl[t].InnerText);
      for(int i=1;i<dgParamList.Parameters.Count;i++)
        args.AddParam(dgParamList.Parameters[i].Name,"",
                resolveParam(dgParamList.Parameters[i].Value,
                dgParamList.Parameters[i].isXPATH, xmlInput));
        string strName;
        if(rdoSequential.Checked)
          strName = txtSequential.Text + t.ToString();
        else
          strName = nl[t].InnerText;

        string strFilename = txtFolder.Text + @"\" + 
                          strName + "." + txtExtension.Text;
        StreamWriter writer = new StreamWriter(strFilename);
        xslFormat.Transform(xmlInput,args,writer);
        writer.Flush();
        writer.Close();
        txtResults.Text+= "Completed transformation of " + 
                                     strFilename + ".\r\n";
    }
    catch
    {
        txtResults.Text += "Transformation " + 
                          (t+1).ToString() + " failed\r\n";
    }
  }
}
catch
{
  txtResults.Text += "Invalid XPath Query Causing Error\r\n";
  return;
}

This is similar in many ways to the simple transformation except we first execute an XPath query against the XML input document using the first parameter stored. We obtain a node-set of matching nodes and proceed to execute one transformation for each node in this node-set. The other parameters are added as before using the resolveParam method.

That's about it. Go light on me people; first article and a bit of an amateur here. I hope someone finds it helpful.

History

16 April 2004: Article written.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United Kingdom United Kingdom
Teacher: Maths and Computing, Secondary School.

First learnt to program in BASIC on an Amstrad CPC464 and then continued messing around with programs at university and ever since. Doesn't know a huge deal about any of it really but enjoys the challenge and is always keen to learn a new trick!

Comments and Discussions

 
GeneralMultiXmlTextWriter class from Oleg Pin
PiscisCetus28-Apr-04 22:28
PiscisCetus28-Apr-04 22:28 
GeneralRe: MultiXmlTextWriter class from Oleg Pin
Paul Baker28-Apr-04 23:14
Paul Baker28-Apr-04 23:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.