Michael H. Pedersen
Michael H. Pedersen

Reputation: 199

xslt recursive template on parent-child data

I'm trying to wrap my mind around xslt. A number of questions here on stackoverflow help ( XSLT templates and recursion and XSLT for-each loop, filter based on variable ) but I'm still kinda puzzled. I guess I'm "thinking of template as functions" ( https://stackoverflow.com/questions/506348/how-do-i-know-my-xsl-is-efficient-and-beautiful )

Anyway...my data is

<Entities>
    <Entity ID="8" SortValue="0" Name="test" ParentID="0" />
    <Entity ID="14" SortValue="2" Name="test2" ParentID="8" />
    <Entity ID="16" SortValue="1" Name="test3" ParentID="8" />
    <Entity ID="17" SortValue="3" Name="test4" ParentID="14" />
    <Entity ID="18" SortValue="3" Name="test5" ParentID="0" />
</Entities>

What I'd like as output is basically a "treeview"

<ul>
    <li id="entity8">
        test
        <ul>
            <li id="entity16">
                test3
            </li>
            <li id="entity14">
                test2
                <ul>
                    <li id="entity17">
                        test4
                    </li>
                </ul>
            </li>
        </ul>
    </li>
    <li id="entity18">
        test5
    </li>
</ul>

The XSLT I have so far is wrong in that it definitely "thinks of templates as functions" and also throws a StackOverflowException (:-)) on execution

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
    <xsl:output method="html" indent="yes"/>

    <xsl:template match="Entities">
        <ul>
            <li>
                <xsl:value-of select="local-name()"/>
                <xsl:apply-templates/>
            </li>
        </ul>
    </xsl:template>

    <xsl:template match="//Entities/Entity[@ParentID=0]">
        <xsl:call-template name="recursive">
            <xsl:with-param name="parentEntityID" select="0"></xsl:with-param>
        </xsl:call-template>
     </xsl:template>

    <xsl:template name="recursive">
        <xsl:param name="parentEntityID"></xsl:param>
        <xsl:variable name="counter" select="//Entities/Entity[@ParentID=$parentEntityID]"></xsl:variable>

        <xsl:if test="count($counter) > 0">
            <xsl:if test="$parentEntityID > 0">
            </xsl:if>
                <li>
                    <xsl:variable name="entityID" select="@ID"></xsl:variable>
                    <xsl:variable name="sortValue" select="@SortValue"></xsl:variable>
                    <xsl:variable name="name" select="@Name"></xsl:variable>
                    <xsl:variable name="parentID" select="@ParentID"></xsl:variable>                    

                    <a href=?ID={$entityID}&amp;ParentEntityID={$parentID}">
                        <xsl:value-of select="$name"/>
                    </a>

                    <xsl:call-template name="recursive">
                        <xsl:with-param name="parentEntityID" select="$entityID"></xsl:with-param>
                    </xsl:call-template>

                </li>           
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

I know how to do this by code, no problem. This time, though, I'm looking for a solution in xslt and for that any help would be greatly appreciated.

Upvotes: 5

Views: 4150

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243539

A solution that is correct and efficient (the currently accepted answer doesn't produce the wanted nested ul elements:

<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:key name="kChildren" match="Entity" use="@ParentID"/>

 <xsl:template match="/*[Entity]">
     <ul>
       <xsl:apply-templates select="key('kChildren', '0')">
            <xsl:sort select="@SortValue" data-type="number"/>
       </xsl:apply-templates>
     </ul>
 </xsl:template>

 <xsl:template match="Entity">
   <li id="entity{@ID}">
      <xsl:value-of select="concat('&#xA;               ', @Name, '&#xA;')"/>
      <xsl:if test="key('kChildren', @ID)">
          <ul>
            <xsl:apply-templates select="key('kChildren', @ID)">
              <xsl:sort select="@SortValue" data-type="number"/>
            </xsl:apply-templates>
          </ul>
      </xsl:if>
   </li>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided XML document:

<Entities>
    <Entity ID="8" SortValue="0" Name="test" ParentID="0" />
    <Entity ID="14" SortValue="2" Name="test2" ParentID="8" />
    <Entity ID="16" SortValue="1" Name="test3" ParentID="8" />
    <Entity ID="17" SortValue="3" Name="test4" ParentID="14" />
    <Entity ID="18" SortValue="3" Name="test5" ParentID="0" />
</Entities>

the wanted, correct result is produced:

<ul>
   <li id="entity8">
               test
<ul>
         <li id="entity16">
               test3
</li>
         <li id="entity14">
               test2
<ul>
               <li id="entity17">
               test4
</li>
            </ul>
         </li>
      </ul>
   </li>
   <li id="entity18">
               test5
</li>
</ul>

Upvotes: 1

Paul Butcher
Paul Butcher

Reputation: 6956

Although call-template and named templates are a very useful feature of the language, if you find yourself preferring them to apply-templates it may be a sign that you are still thinking in functions rather than templates. This is particularly true if the first thing you do in a named template is select a nodeset on which to operate.

Here is a simple version of what you are trying to do.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" />
    <xsl:template match="/">
        <ul>
           <xsl:apply-templates select="Entities/Entity[@ParentID=0]" />
        </ul>
    </xsl:template>

    <xsl:template match="Entity">
        <li>
           <xsl:value-of select="@Name" />
           <xsl:apply-templates select="../Entity[@ParentID=current()/@ID]" />
        </li>
    </xsl:template>
</xsl:stylesheet>

Note that there is no need for the counter, as the value of the "parent" provides the necessary context.

Also note that all Entities behave in the same way, regardless of where they are in the tree, they contain their @Name value, and apply the template to any Entity objects whose @ParentID matches the @ID of the current level.

Upvotes: 7

Related Questions