XSLT Sorting - how to sort xml childnodes inside a parent node with an attribute

JimXC picture JimXC · Feb 1, 2013 · Viewed 20.3k times · Source

What started out as a simple thing has turned out quite troublesome for XSLT noob.

Trying to sort childnodes/descending but, after adding an attribute to their parent node, I receive an error when debugging in VS2010:

"Attribute and namespace nodes cannot be added to the parent element after a text, comment, pi, or sub-element node has already been added."

Suppose I have this simple XML:

<posts>
    <year value="2013">
        <post postid="10030" postmonth="1">
            <othernode></othernode>
            <othernode2></othernode2>
        </post>
        <post postid="10040" postmonth="2">
            <othernode></othernode>
            <othernode2></othernode2>
        </post>
        <post postid="10050" postmonth="3">
             <othernode></othernode>
             <othernode2></othernode2>
        </post>
    </year>
    <year value="2012">
        <post postid="10010" postmonth="1">
            <othernode></othernode>
            <othernode2></othernode2>
        </post>
        <post postid="10015" postmonth="2">
            <othernode></othernode>
            <othernode2></othernode2>
        </post>
        <post postid="10020" postmonth="3">
             <othernode></othernode>
             <othernode2></othernode2>
        </post>
    </year>
</posts>

I pass a XPATH to a xmldatasource to retrieve the relevant <year> node, e.g. 2013. Then I need to sort its child <post> nodes descending using postid, so for <year value=2013>, postid=10050 would show up first when rendered.

So, to be clear: I'm only interested in sorting inside one <year> node.

Before I split the nodes into separate nodes (i.e. xml was /posts/post) the following XSLT worked:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="xml" indent="yes" omit-xml-declaration="no"/>
    <xsl:strip-space elements="*"/>
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()">
                <xsl:sort select="@postid" data-type="text" order="descending"/>
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

Now the xmldatasource is empty when running due to the above error. If I pass ascending into the order the same xml is returned obviously (no transformation)

Question: how to update the XSLT above (or new) to accommodate the parent node attribute (<year value="">)? Through research, an answer said "I need to added attribute creation before element creation". This makes sense as watching the debugger, the childnodes are formed in desc order, but the year tag is missing its attribute. But I don't have a clue really about XSLT. Can't see it being too complicated but just don't know the language.

Any help, greatly appreciated. Thanks

Answer

JLRishe picture JLRishe · Feb 1, 2013

So are you saying that you're only passing part of your XML document (one <year> node) to the XSLT processor?

You should use a separate template for year, so that that's the only template that uses sorting. How is the following:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes" omit-xml-declaration="no"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="year">
    <xsl:copy>
      <xsl:apply-templates select="@*" />
      <xsl:apply-templates select="post">
        <xsl:sort select="@postid" data-type="number" order="descending"/>
      </xsl:apply-templates>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

I think the above is a better approach, but I think the root cause of your error is that it was mingling the attributes in with the elements when it did the sorting. Your original XSLT probably would run without error if you simply did this:

<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*">
        <xsl:apply-templates select="node()">
            <xsl:sort select="@postid" data-type="text" order="descending"/>
        </xsl:apply-templates>
    </xsl:copy>
</xsl:template>