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

21,302

Solution 1

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>

Solution 2

This transformation:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <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="*">
      <xsl:sort select="@postid" data-type="number" order="descending"/>
    </xsl:apply-templates>
  </xsl:copy>
 </xsl:template>
</xsl:stylesheet>

when applied on the provided XML document:

<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>

produces the wanted, correct result:

<posts>
   <year value="2013">
      <post postid="10050" postmonth="3">
         <othernode/>
         <othernode2/>
      </post>
      <post postid="10040" postmonth="2">
         <othernode/>
         <othernode2/>
      </post>
      <post postid="10030" postmonth="1">
         <othernode/>
         <othernode2/>
      </post>
   </year>
   <year value="2012">
      <post postid="10020" postmonth="3">
         <othernode/>
         <othernode2/>
      </post>
      <post postid="10015" postmonth="2">
         <othernode/>
         <othernode2/>
      </post>
      <post postid="10010" postmonth="1">
         <othernode/>
         <othernode2/>
      </post>
   </year>
</posts>
Share:
21,302
JimXC
Author by

JimXC

freelance web developer &amp; designer | .net MVC | HTML | CSS | jQuery | React

Updated on July 09, 2022

Comments

  • JimXC
    JimXC almost 2 years

    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

  • JimXC
    JimXC over 11 years
    That's it. Fantastic it works. Thank you so much sir! Yes I'm only passing one year node to the XSLT. I choose the year/posts I want to display. Will try to figure out your new structure and learn to read xslt.
  • Dimitre Novatchev
    Dimitre Novatchev over 11 years
    Why would one use data-type="text" in this case? this obviously produces wrong results if postid values can be of different lengths.
  • JimXC
    JimXC over 11 years
    Confirmed this also works. Again, for my own benefit, you're using number in the data-type - ok understood. But previous poster used select=post in the apply-template, you use *. What's the difference? Assuming you're catering for any named nodes whereas other is post specific?
  • JLRishe
    JLRishe over 11 years
    That's a good point. I used it because that's what JimXC's original XSLT used and it wasn't the focus of his question, plus I hadn't noticed that the IDs in his example are numeric. If the IDs are guaranteed to be numeric, then data-type="numeric" would be preferable.
  • Dimitre Novatchev
    Dimitre Novatchev over 11 years
    @JimXC, Whenever we know that any child element of the current node is a post, using * could be more efficient as this avoids a check by the XSLT processor whether a child is a post or not. Also, the code is shorter this way and writing it is faster and easier.
  • Dimitre Novatchev
    Dimitre Novatchev over 11 years
    @JLRishe, Not only "would be preferreble", but "must be used" -- sorting based on numeric sort-key must specify data-type="number"
  • JLRishe
    JLRishe over 11 years
    @DimitreNovatchev Both will behave identically if the values are all the same length, but fundamentally you are right: if the IDs are all guaranteed to be numbers, then he should definitely use data-type="number" (I realize I accidentally typed "numeric" in my comment). Since I can't say with complete certainty that Jim's IDs will always be numeric, I'll leave my answer as it is for now, pending clarification from him.
  • JimXC
    JimXC over 11 years
    @JLRishe Yes, they are numeric IDs and always will be. But this wasn't clear from question and it already had text. As you said, wasn't core to the question. Thanks both for the specifics.