A Dynamic XSL Transformation
 
Published: 02 Sep 2004
Unedited - Community Contributed
Abstract
What do you do when you need to transform an XML file but will not know the exact structure of the XML file? This article will demonstrate a method for performing a transformation on an XML where the nodes of the document could vary.
by Michelle Beall
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 28688/ 77

Introduction

A project I was involved in required me to generate a HTML table from an XML file.  Simple enough, right?  The transformation would be easy; for each node create a new table row and each attribute would be a table cell.  This would be a nice project to get introduced to XML and XSL.

The catch was that although the XML nodes were defined, the attribute names could vary from one time to the next.  The column title had to reflect the dynamic attribute names and the HTML table had to be displayed in a certain format.  So the challenge came when trying to figure out how to create the XSL and display empty cells when an attribute was not present, especially when I would not know the attribute name until runtime.

The XML is generated by a Sharepoint Web Service.  The attribute names are names of custom fields created by the user in a Sharepoint Document Library.  Sharepoint's list web service returns some standard attributes by default, those would always be known, as well as any columns specified by the query.

Hopefully this article will give you some ideas as to how to approach similar or more complicated XSL challenges.

The following was used in generating the example:

  • Microsoft Visual Studio .NET 2003 (1.1 Framework)
  • Microsoft Visual C#
  • Web service running on IIS 5
  • XML
  • XSL

 

ASPX and Sample XML

After creating a blank web project in Visual Studio I added the following code to the page's class in the code behind file.  This was my test page to test my transformation against a sample XML file.  An XML control is added to the web form and it is used to display the XML by applying the XSLT.  Prior to displaying the XML it updates the XML document to add any missing attributes to each of the data elements by comparing it to an array of expected attributes.

protected System.Web.UI.WebControls.Xml Xml1;       
  
// XmlDocument class for loading an xml file. 
private XmlDocument _doc; 
// XslTransform class for loading an xslt file. 
private XslTransform _transform; 
// String: Path to xml file. 
private string _xmlPath; 
// String: Path to xslt file. 
private string _xslPath;

private void Page_Load(object sender, System.EventArgs e) 
{ 
    // Get paths for XML and XSLT files 
    _xmlPath = Server.MapPath("data.xml"); 
    _xslPath = Server.MapPath("trans.xslt");

    // Instantiate the XmlDocument Class 
    _doc = new XmlDocument(); 
    _doc.XmlResolver = null; 
    _doc.Load(_xmlPath);

    // Array storing selected attributes to be displayed 
    // This array would be generated at runtime to match the web service query fields
    string[] ColumnNames = new string[5]; 
    ColumnNames[0] = "ows_Title"; 
    ColumnNames[1] = "ows_Language"; 
    ColumnNames[2] = "ows_Level2"; 
    ColumnNames[3] = "ows_Level3"; 
    ColumnNames[4] = "ows_Version"; 

    // Retrieve all z:row elements in nodeList 
    XmlElement element = _doc.DocumentElement; 
    XmlNodeList nodeList = element.FirstChild.ChildNodes; 

    // for each z:row element determine if all attributes are present
    // by comparing to array 
    // if not, then add missing attributes 
    foreach (XmlNode node in nodeList)
    { 
        // get all attributes for current element 
        XmlAttributeCollection attrColl = node.Attributes;
         
        // find each column in the attribute collection 
        foreach(string s in ColumnNames) 
        { 
            // if column is missing create attribute and add to element 
            if (attrColl[s] == null) 
            { 
                string prevValue = ""; 
                XmlAttribute attr = _doc.CreateAttribute(s); 
                attr.Value = " "; 
                int columnIndex = Array.IndexOf(ColumnNames, s); 
                if (columnIndex == 0) 
                { 
                    // insert attribute as first node 
                    prevValue = ColumnNames[columnIndex].ToString(); 
                    node.Attributes.Prepend(attr); 
                } 
                else 
                { 
                    // insert attribute in correct location 
                    // assumes previous attribute exists 
                    prevValue = ColumnNames[columnIndex - 1].ToString(); 
                    node.Attributes.InsertAfter(attr, attrColl[prevValue]); 
                } 
            } 
        } 
    } 

    // ASSERT: now have a well-formed document with no display attributes missing
    // Instantiate the XslTransform Class 
    _transform = new XslTransform(); 
    _transform.Load(_xslPath);      
  
    // Render the XML document
    Xml1.Document = _doc;
    Xml1.Transform = _transform;
} 

The following sample XML is based on the returned XML from calling Sharepoint's List Web Service.  This is saved as data.xml in the project.

<listitems xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" 
  xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" 
  xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema" 
  xmlns="http://schemas.microsoft.com/sharepoint/soap/">
  <rs:data ItemCount="50"> 
    <z:row ows_Title="Title" 
         ows_Language="EN" 
         ows_Level2="Level2"
         ows_Level3="Level3" 
         ows_Version="Version" 
         ows_Last_x0020_Modified="21;#2004-01-28 08:27:16" 
         ows_ID="21" 
         ows_owshiddenversion="2" 
         ows_FSObjType="21;#0"  
         ows_FileLeafRef="21;#Title.doc"  
         ows_Modified="2004-01-28 08:27:15" 
         ows_FileRef="21;#sites/sitename/doclibname/Title.doc" 
         ows_Editor="7;#Editor Name" 
         ows_DocIcon="doc"/> 
    <z:row ows_Title="Title2" 
         ows_Level2="Level2"
         ows_Level3="Level3" 
         ows_Version="Version" 
         ows_Last_x0020_Modified="21;#2004-01-28 08:27:16" 
         ows_ID="21" 
         ows_owshiddenversion="2" 
         ows_FSObjType="21;#0"  
         ows_FileLeafRef="21;#Title2.doc"  
         ows_Modified="2004-01-28 08:27:15" 
         ows_FileRef="21;#sites/sitename/doclibname/Title2.doc" 
         ows_Editor="7;#Editor Name" 
         ows_DocIcon="doc"/> 
    <z:row ows_Title="Title3" 
         ows_Language="EN" 
         ows_Level2="Level2"
         ows_Version="Version" 
         ows_Last_x0020_Modified="21;#2004-01-28 08:27:16" 
         ows_ID="21" 
         ows_owshiddenversion="2" 
         ows_FSObjType="21;#0"  
         ows_FileLeafRef="21;#Title3.doc"  
         ows_Modified="2004-01-28 08:27:15" 
         ows_FileRef="21;#sites/sitename/doclibname/Title3.doc" 
         ows_Editor="7;#Editor Name" 
         ows_DocIcon="doc"/>
  </rs:data>
</listitems>

This sample shows three z:row elements.  The second element has the ows_Language attribute missing and the third element has the ows_Level3 attribute missing.

So how do we transform this data into a HTML table?

 

XSL

Once the XML document has been standardized, the trans.xslt file performs the transformation of the standardized XML document into a HTML table.  The first time the z:row template is applied the table header row is created using the names of the expected attributes.  The table begins with two static columns, Type and Name, followed by a series of columns for each dynamic attribute field, and finally another static column for the Modified date.  For each z:row element a new table row is generated and each cell created. 

Type Name Dynamic Fields Modified
... data ...
... data ...

The XML document had been modified to add missing attributes with empty string values to the z:row elements.  For the dynamic attributes a test is performed to ensure that a nobreakspace is displayed when the attribute has no value:

<xsl:if test="not(normalize-space(.))">
    <xsl:value-of select="'&#160;'"/>
</xsl:if>

The assumption was that the standard data fields returned by the Sharepoint web service would always have data.

The complete XSLT follows and was saved as trans.xslt in the project.

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" 
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      xmlns:rs="urn:schemas-microsoft-com:rowset"
      xmlns:z="#RowsetSchema"
      xmlns:msxsl="urn:schemas-microsoft-com:xslt"
      xmlns:user="user"
      exclude-result-prefixes="msxsl user">
 <xsl:output method="html" version="4.0" encoding="iso-8859-1" indent="yes"/>  
 
 <xsl:template match="rs:data"> 
  <!-- Document List -->
  <table cellspacing="0" cellpadding="0">
   <xsl:apply-templates select="z:row" />
  </table> 
 </xsl:template>
 
 <xsl:template match="z:row">
  <xsl:if test="position() = '1'">
   <!-- Table Header Row -->
   <tr>
    <!-- Fixed Column "Type" -->
    <td>
     <xsl:attribute name="class">ms-vh2</xsl:attribute>
     <xsl:attribute name="nowrap" />
     <xsl:text>  </xsl:text>
     Type
    </td>
    <!-- Fixed Column "Name" -->
    <td>
     <xsl:attribute name="class">ms-vh2</xsl:attribute>
     <xsl:attribute name="nowrap" />
     <xsl:text>  </xsl:text>
     Name 
    </td>
    <!-- Variable Columns -->
    <xsl:for-each select="./@*">
     <xsl:choose>
      <xsl:when test="name() = 'ows_Last_x0020_Modified'" />
      <xsl:when test="name() = 'ows_ID'" />
      <xsl:when test="name() = 'ows_owshiddenversion'" />
      <xsl:when test="name() = 'ows_FSObjType'" />
      <xsl:when test="name() = 'ows_FileLeafRef'" />
      <xsl:when test="name() = 'ows_Modified'" />
      <xsl:when test="name() = 'ows_FileRef'" />
      <xsl:when test="name() = 'ows_Editor'" />
      <xsl:when test="name() = 'ows_DocIcon'" />
      <xsl:otherwise>
       <td>
        <xsl:attribute name="class">ms-vh2</xsl:attribute>
        <xsl:attribute name="nowrap" />
        <xsl:text>  </xsl:text>
        <xsl:value-of select="substring-after(name(),'_')" />
       </td>
      </xsl:otherwise>
     </xsl:choose>   
    </xsl:for-each>
    <!-- Fixed Column "Modified" -->
    <td>
     <xsl:attribute name="class">ms-vh2</xsl:attribute>
     <xsl:attribute name="nowrap" />
     <xsl:text>  </xsl:text>
     Modified
    </td>  
   </tr>
  </xsl:if> 
   
  <!-- Table Detail Rows -->
  <tr>
   <!-- Fixed Column "Type" -->
   <td>
    <xsl:attribute name="class">ms-vb2</xsl:attribute> 
    <xsl:attribute name="nowrap" />
    <img>
     <xsl:attribute name="src">http://TopLevelDomain/_layouts/images/ic
      <xsl:value-of select="@ows_DocIcon"/>.gif
     </xsl:attribute>
     <xsl:attribute name="align">texttop</xsl:attribute>
     <xsl:attribute name="border">0</xsl:attribute>
    </img>
   </td>
   <!-- Fixed Column "Name" -->
   <td>
    <xsl:attribute name="class">ms-vb2</xsl:attribute> 
    <xsl:attribute name="nowrap" />
    <a> 
     <xsl:attribute name="href">http://TopLevelDomain/
      <xsl:value-of select="substring-after(@ows_FileRef,'#')"/>
     </xsl:attribute>    
     <xsl:value-of select="substring(substring-after(@ows_FileLeafRef,'#'), 1, 
                             string-length(
                               substring-after(@ows_FileLeafRef,'#'))-4)"/>
    </a>
   </td>
   <!-- Variable Columns -->
   <xsl:for-each select="@*">
    <xsl:choose>
     <xsl:when test="name()= 'ows_Last_x0020_Modified'" />
     <xsl:when test="name()= 'ows_ID'" />
     <xsl:when test="name()= 'ows_owshiddenversion'" />
     <xsl:when test="name()= 'ows_FSObjType'" />
     <xsl:when test="name()= 'ows_FileLeafRef'" />
     <xsl:when test="name()= 'ows_Modified'" />
     <xsl:when test="name()= 'ows_FileRef'" />
     <xsl:when test="name()= 'ows_Editor'" />
     <xsl:when test="name()= 'ows_DocIcon'" />
     <xsl:otherwise>
      <td>
       <xsl:attribute name="class">ms-vb2</xsl:attribute> 
       <xsl:if test="not(normalize-space(.))">
        <xsl:value-of select="'&#160;'"/>
       </xsl:if>
       <xsl:value-of select="." />
      </td>
     </xsl:otherwise>
    </xsl:choose>    
   </xsl:for-each>
   <!-- Fixed Column "Modified" -->
   <td>
    <xsl:attribute name="class">ms-vb2</xsl:attribute> 
    <xsl:attribute name="nowrap" />
    <xsl:value-of select="@ows_Modified"/>
   </td> 
  </tr> 
   </xsl:template>
 </xsl:stylesheet>

Conclusion

This example demonstrated how to take an XML file returned from a web service and generate a HTML table with dynamic columns based on the query fields to the web service.

We created an ASPX with an XML control which we used to view the XML document and apply the XSLT to the XML document.  We modified the XML document to insert any missing expected attributes since the web service would only return an attribute if it had a value.

The XSLT built a HTML table and for each attribute that had a blank value we displayed a nobreak space.  The XSLT does not need to know the attribute names or the number of attributes.  This is governed in the ASPX page when we clean up the XML returned by the web service.

I hope this helps some people.



User Comments

No comments posted yet.






Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2019 ASPAlliance.com  |  Page Processed at 2019-04-19 5:09:12 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search