10

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
  • 275
  • 1
  • 4
  • 12

2 Answers2

11

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>
JLRishe
  • 99,490
  • 19
  • 131
  • 169
  • 1
    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. – JimXC Feb 01 '13 at 14:08
  • Why would one use `data-type="text"` in this case? this obviously produces wrong results if `postid` values can be of different lengths. – Dimitre Novatchev Feb 01 '13 at 14:09
  • 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. – JLRishe Feb 01 '13 at 14:13
  • @JLRishe, Not only "would be preferreble", but "must be used" -- sorting based on numeric sort-key *must* specify `data-type="number"` – Dimitre Novatchev Feb 01 '13 at 14:20
  • @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. – JLRishe Feb 01 '13 at 14:51
  • @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. – JimXC Feb 01 '13 at 15:21
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>
Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
  • 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? – JimXC Feb 01 '13 at 14:13
  • 1
    @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 Feb 01 '13 at 14:18