XtremeData
LANGUAGES: C#
ASP.NET VERSIONS: 1.1
XML Date Sorts
Create XSLT Extension Objects to Sort XML Data
By Dan Wahlin
Extensible Stylesheet Language Transformations (XSLT) provides a flexible mechanism for sorting numbers and text found in XML documents. However, version 1.0 doesn't support sorting dates with the xsl:sort element. This is unfortunate, because dates are quite common in applications.
Consider the XML document shown in Figure 1. Using strictly native XSLT/XPath 1.0 features, it's difficult to sort the Customer nodes based on dates contained in the CustomerSince child nodes, because the xsl:sort element's data-type attribute only supports text and numeric types. In this article, I'll demonstrate how you can get around this sorting issue (without resorting to multiple sorts based on date substrings) by using an XSLT extension object written in C#.
...
...
...
...
Figure 1: Sample XML document. It's commonly necessary to sort nodes in an XML document based on dates. The extension object demonstrated in this article allows sorting to occur based on the CustomerSince node.
.NET XSLT extension objects are compiled classes that provide a powerful way to provide functionality otherwise not available in the XSLT language. By using extension objects, XSLT stylesheets can perform advanced math calculations, tie into data sources other than XML, and perform more common tasks, such as sorting dates.
Although extension objects have many advantages, they're not without disadvantages. It's important to keep in mind that XSLT stylesheets that leverage extension objects are less portable across operating systems and development platforms. Of course, this isn't a problem if your stylesheets don't need to be ported to different platforms, but if portability is an issue, you may want to consider using specialized XSLT libraries, such as those found at http://www.exslt.org.
XSLT Scripting
Before detailing how to build an extension object, let's take a moment to discuss a viable alternative to extension objects referred to as XSLT scripting. Although it isn't part of the XSLT 1.0 specification, this technique involves embedding C#, VB .NET, or JScript code directly into an XSLT stylesheet. To help with performance, the embedded code is compiled into MSIL the first time it's executed.
The code in Figure 2 demonstrates how a C# function named ToDateString can be embedded in an msxsl:script element. The msxsl namespace prefix must be defined within the XSLT stylesheet. It must also be associated with the urn:schemas-microsoft-com:xslt Uniform Resource Identifier (URI). The value specified in the implements-prefix attribute (the DateFunc namespace prefix in this case) can be associated with any namespace URI.
public string ToDateString(XPathNodeIterator node)
{
node.MoveNext();
return DateTime.Parse(
node.Current.Value).ToLongDateString();
}
]]>
Figure 2: XSLT stylesheets written to run within the .NET Framework can leverage script blocks as shown here. The ToDateString method converts dates such as 1-2-2002 to a friendlier format, such as Wednesday, January 02, 2002.
Consider an example of calling the ToDateString method from within an xsl:value-of element, to convert the data within the CustomerSince element to a friendlier date format:
select="DateFunc:ToDateString(CustomerSince)"/> You can see that XSLT scripting makes it easy to extend
stylesheet functionality with minimal effort. However, code placed in script
blocks is only useful to the XSLT stylesheet in which it's embedded. It can
also be more tedious to write, especially when classes outside the System.Xml
assembly need to be used. Plus, you don't benefit from IntelliSense as you
develop the code. (And let's face it: Many of us are addicted to IntelliSense.)
Now that you've been introduced to XSLT scripts, let's move on and discuss the
process of creating XSLT extension objects. Sort Dates in XSLT with Extension
Objects Writing XSLT extension object code is no different than
writing code for any .NET application. Simply create a class with methods that
perform the activities you desire. No inheritance or interface implementation
is required. To provide date sorting capabilities for XSLT stylesheets, I
created a class named XSLTDateTime that has two methods: SortByDate
and ToDateString. Take a look at the complete code for SortByDate
(see Figure 3). Notice that the method accepts three parameters: the context
node being acted on by the XSLT processor, the node (or nodes) to return from
the method after an XPath statement is executed, and the node containing the
date data on which the sort will be based. public
XPathNodeIterator SortByDate(XPathNodeIterator node, string nodeToSelect, string
dateNodeName) { string xpath = nodeToSelect +
"[" + dateNodeName + " != '']"; // Check if we sort on an attribute or text node. string sort = (dateNodeName.IndexOf("@") != -1)? dateNodeName:dateNodeName + "/text()"; // Position nav on first node of node set. node.MoveNext(); XPathNavigator nav =
node.Current; // Compile XPath expression so we can add a sort to it. XPathExpression exp =
nav.Compile(xpath); DateComparerXSLT dc = new DateComparerXSLT(); exp.AddSort(sort,dc); // Select nodes so we can see the sort. return (XPathNodeIterator)nav.Select(exp); } Figure 3: The SortByDate
method executes an XPath statement that returns one or more nodes sorted based
on dates. The method relies on the XPathNavigator class and other helper
classes such as XPathExpression and XPathNodeIterator. Once the SortByDate method is called, the node
parameter's MoveNext method is called to move to the first node in the
node set. This is necessary to get the correct context node being processed by
the XSLT processor. The nodes object's Current property is then called
to access the underlying XPathNavigator object named nav. The nav
object is needed to compile an XPath statement and return an XPathExpression
object. It's also needed to execute an XPath statement and locate specific
nodes in the source XML document. Because the XPathExpression class' AddSort
method doesn't natively support sorting by date, I created another class named DateComparerXSLT
that implements the IComparer interface (see Figure 4). This custom
object is passed to the AddSort method so that dates can be properly
sorted. IComparer contains a single method named Compare that's
used to do comparisons between objects. The integer value returned from Compare
is used to properly sort data when the XPath statement is executed. public class
DateComparerXSLT : IComparer { public
DateComparerXSLT() {} public int
Compare(object date1, object date2) { int intResult; DateTime d1 =
Convert.ToDateTime(date1); DateTime d2 =
Convert.ToDateTime(date2); intResult =
DateTime.Compare(d1,d2); return intResult; } } Figure 4: Performing custom comparisons between
objects is accomplished by implementing the IComparer interface. The Compare
method returns -1, 0, or 1 depending on how the two objects being compared
match up. After the sort is added to the XPathExpression
object (named exp) shown earlier in
Figure 3, this code is called to execute the XPath statement contained in exp and perform the sort: // Select nodes so we can iterate
through them, // and see the sort. return (XPathNodeIterator)nav.Select(exp); Using the XSLT Extension Object To call the SortByDate method from within an XSLT
Stylesheet, the code controlling the XSLT transformation must pass the
extension object to the stylesheet. To do this, an XslTransform object
must be created along with an XsltArgumentList object. The XsltArgumentList
has a method named AddExtensionObject that can be used to associate an
extension object with a stylesheet. The code in Figure 5 creates these objects
and performs the XSLT transformation. string dateType = "LongDate"; StringWriter sw = new StringWriter(); XPathDocument doc = new XPathDocument( Server.MapPath("XML/Customers.xml")); XslTransform trans = new XslTransform(); // Load stylesheet and provide evidence so extension // object can be used. trans.Load(new XPathDocument(Server.MapPath( "XSLT/Customers.xslt")).CreateNavigator(), new XmlUrlResolver(),
this.GetType().Assembly.Evidence); // Create and add extension object and DateType param. // DateType param determines how dates are displayed. XSLTDateTime sorter = new XSLTDateTime(); XsltArgumentList args = new XsltArgumentList(); args.AddExtensionObject( "urn:xsltExtension-XSLTDateTime", sorter); args.AddParam("DateType", "", dateType); trans.Transform(doc, args, sw, null); Figure 5: The XsltArgumentList
class can be used to add parameter data and extension objects to stylesheets
for use in an XSLT transformation. When AddExtensionObject is called, the namespace
URI (associated with the extension object namespace prefix defined in the XSLT stylesheet)
must be passed along with the actual object instance. The stylesheet knows how
to handle the extension object because of the inclusion of a custom namespace
URI. Notice that the URI string passed to AddExtensionObject in Figure 5
matches the one defined by the XSLTDateTime namespace prefix shown here: < xsl:stylesheet
version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:XSLTDateTime="urn:xsltExtension-XSLTDateTime"> ... xsl:stylesheet> Once the extension object is passed to the stylesheet, the
XSLTDateTime object's SortByDate method can be called from within
the stylesheet to retrieve a node set containing Customer nodes that are sorted
based on date data found in the CustomerSince child node (see Figure 6). "XSLTDateTime:SortByDate(.,'Customer','CustomerSince')"> "XSLTDateTime:ToDateString(CustomerSince,$DateType)"/> Figure 6: Calling
an extension object method from within an XSLT stylesheet is accomplished by
prefixing the method name with a namespace prefix that's associated with the
extension object. The XSLTDateTime extension object contains an
additional method named ToDateString that can be called to convert
numeric dates to long and short formats (see Figure 7). Output is generated by
calling the SortByDate and ToDateString methods from within an
XSLT stylesheet (see Figure 8). public string ToDateString(XPathNodeIterator node, string dateType) { // Try to convert
dateType to DateTypeEnum. DateTypeEnum type; try { type =
(DateTypeEnum)Enum.Parse( typeof(DateTypeEnum),dateType,true); } catch { return "Invalid
DateTypeEnum type passed. " + "You can
use 'LongDate' or 'ShortDate'"; } node.MoveNext(); // Position nav on first node. XPathNavigator nav =
node.Current; string val = GetNodeValue(nav); switch (type) { case
DateTypeEnum.ShortDate: try { return
DateTime.Parse(val).ToShortDateString(); } catch { return val; } case
DateTypeEnum.LongDate: try { return
DateTime.Parse(val).ToLongDateString(); } catch { return val; } default: return val; } } Figure 7: The ToDateString
method converts a string value of "LongDate" or "ShortDate" to a custom
enumeration, and then converts the appropriate date data to the desired output
format. Conclusion Certainly, XSLT extension objects aren't needed every time
XML data is transformed, but they can be quite useful in certain situations. In
this article you've seen one potential way to leverage extension objects to
sort XML data based on dates. You've also seen how code can be embedded
directly into XSLT stylesheet script blocks. Looking to the future of XSLT, in version 2.0 you won't be
required to go to a lot of extra effort to sort dates while transforming XML
documents because sorting on specific data types defined in XML schemas will be
natively supported (as of this writing XSLT 2.0 was a "Working Draft"). Until
XSLT 2.0 is released and supported by the .NET Framework, the XSLTDateTime
extension object provides a fairly simple way to handle date sorting. The sample code in this article is available for download.
To view a live example of the code, visit Dan's XML for ASP.NET Developers Web
site: http://www.xmlforasp.net/CodeSection.aspx?csID=104. Dan Wahlin
(Microsoft Most Valuable Professional for ASP.NET and XML Web services) is the
president of Wahlin Consulting and founded the XML for ASP.NET Developers Web
site (http://www.XMLforASP.NET), which
focuses on using XML and Web services in Microsoft's .NET platform. He's also a
corporate trainer and speaker, and teaches XML and .NET training courses around
the United States. Dan coauthored Professional
Windows DNA (Wrox, 2000) and ASP.NET:
Tips, Tutorials and Code (SAMS,
2001), and authored XML for ASP.NET Developers (SAMS, 2001).
Figure 8: The data shown here is
sorted based on date data found within a node named CustomerSince (see Figure
1). Notice that in addition to properly sorting by date, the numeric dates
found in the XML document have been converted to friendlier string versions of
the date.