Link to home
Start Free TrialLog in
Avatar of metalmickey
metalmickey

asked on

Keys

Im trying to understand the nodesets that are evaluated by 2 keys from a tutorial listed here.

http://www.codeproject.com/soap/groupxml.asp

ive created a duplicate of the tutorial and im trying to write in the nodeset which is returned by this statement

<xsl:for-each select="$lstEmployee[generate-id(.) = generate-id(key('keyEmployeeID', EmployeeID)[1])]">

im using a step by step aproach to list the xml that is held in each key (marked in red)

http://homepages.nildram.co.uk/~natkins/key/Using-Keys.htm

can someone check that firstly ive selected the correct nodes and secondly list the xml that is held in the above for-each stament.

Thanks for your help

MetalMickey
Avatar of rdcpro
rdcpro
Flag of United States of America image

To me, this seems much simpler:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <!-- Define keys used to group elements -->
      <xsl:key name="keyTeamID" match="Employee" use="TeamID"/>
      <xsl:key name="keyEmployeeID" match="Employee" use="EmployeeID"/>
      <xsl:template match="Employees">
            <html>
                  <head>
                        <title>Employee Hours By Team</title>
                        <link type="text/css" rel="stylesheet" href="groupxml.css"/>
                  </head>
                  <body>
                        <h3>Employee Hours By Team</h3>
                        <table>
                              <!-- Process each Team -->
                              <xsl:for-each select="Employee[generate-id(.) = generate-id(key('keyTeamID', TeamID)[1])]">
                                    <!-- Convenience variable to hold the employees for this team.  -->
                                    <xsl:variable name="vTeamEmployees" select="/Employees/Employee[TeamName=current()/TeamName]"/>
                                    <tr>
                                          <td colspan="4" class="DarkBack">TEAM: <xsl:value-of select="TeamName"/>
                                          </td>
                                          <td colspan="1" class="DarkBack RightJustified">HOURS</td>
                                    </tr>
                                    <!-- Show details for Employees in Team -->
                                    <xsl:for-each select="$vTeamEmployees[generate-id(.) = generate-id(key('keyEmployeeID', EmployeeID)[1])]">
                                          <tr>
                                                <td colspan="4">
                                                      <xsl:value-of select="Name"/>
                                                      <xsl:value-of select="Surname"/>
                                                </td>
                                                <td colspan="1" class="RightJustified">
                                                      <!-- Show the total hours for the current Employee -->
                                                      <xsl:value-of select="sum($vTeamEmployees[EmployeeID=current()/EmployeeID]/Hours)"/>
                                                </td>
                                          </tr>
                                    </xsl:for-each>
                                    <tr>
                                          <td colspan="4" class="LightBack RightJustified">Sub-Total</td>
                                          <td colspan="1" class="LightBack RightJustified">
                                                <!-- Show the total hours for all Employees in the Team -->
                                                <xsl:value-of select="sum($vTeamEmployees/Hours)"/>
                                          </td>
                                    </tr>
                              </xsl:for-each>
                              <tr>
                                    <td colspan="4" class="RightJustified DarkBack">Grand Total</td>
                                    <td colspan="1" class="RightJustified DarkBack">
                                          <!-- Show Grand Total of hours for all Employees -->
                                          <xsl:value-of select="sum(Employee/Hours)"/>
                                    </td>
                              </tr>
                        </table>
                  </body>
            </html>
      </xsl:template>
</xsl:stylesheet>


I generally use variables only in cases where I can re-use the nodelist.  In this case, I use the same nodelist three times:

/Employees/Employee[TeamName=current()/TeamName]

So I set a variable to that, and when I need hours, I just use:

<xsl:value-of select="sum($vTeamEmployees/Hours)"/>

and if I need to filter the nodelist by employee ID, I just use:

<xsl:value-of select="sum($vTeamEmployees[EmployeeID=current()/EmployeeID]/Hours)"/>

And to iterate over the nodelist, grouping by EmployeeID, I just use:

<xsl:for-each select="$vTeamEmployees[generate-id(.) = generate-id(key('keyEmployeeID', EmployeeID)[1])]">



For me, using multiple keys is simpler if you just use each key to create an iterator (whether it's a for-each or an apply-templates) that's used to create the page structure (a table, in this case).  Then for the data, you simply use the values in the current() node to filter the entire document's nodes.  I would avoid all that call-templates stuff.

Regards,
Mike Sharp
ASKER CERTIFIED SOLUTION
Avatar of rdcpro
rdcpro
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of metalmickey
metalmickey

ASKER

Thanks Mike,

I get the principle of storing nodesets, it's the visualising them thats my problem. I've got a project that involves an xml record that isnt too dis-similar to the employees example

What nodeset does this key hold ?

<xsl:key name="keyEmployeeID" match="Employee" use="concat(TeamID,EmployeeID)"/>

Is there a simple way of outputting the nodesets of different keys so i can see whats in them?

Thanks

MM
I think I see your difficulty.  This nodeset:

<xsl:key name="keyEmployeeID" match="Employee" use="concat(TeamID,EmployeeID)"/>

has *every* Employee node in the document!  This nodeset:

<xsl:key name="keyEmployeeID" match="Employee" use="concat(Surname,FirstName)"/>

would have the exact same nodes.  It's just a way of indexing them by a key value, rather than by it's relative position.  The first one indexes the nodes by the combination of TeamID and EmployeeID, the second one indexes the exact same nodes by Surname and FirstName.  

So If I want a particular Employee node, instead of using an XPath expression relative to some other node, I can use a key value.  It's exactly like a dictionary object, or an associative array.  Think of the nodeset like:

Key                                          Value

"SharpMike"                               "//Employee[Surname = 'Sharp' and FirstName = 'Mike' "
"MickeyMetal"                             "//Employee[Surname = 'Mickey' and FirstName = 'Metal' "
etc
Drat.  accidentally clicked "Submit"....Anyway:

The reason we use this for grouping doesn't have anything to do with the key, but with the XPath expression we use to select the nodes in a for-each or apply-templates.  Keys make it easier to do the grouping, but only because it's a convenient way to identify certain nodes.  In other words, this would have the same effect, but wouldn't execute as fast:


<xsl:for-each select="Employee[generate-id() = generate-id(//Employee[TeamID = current()/TeamID and EmployeeID = current()/EmployeeID])]">

What makes it work is the generate-id() works on only the first node in the nodeset defined by:

(//Employee[TeamID = current()/TeamID and EmployeeID = current()/EmployeeID])

So the current node is the same one as the first occurance of Employee with the same TeamID and EmployeeID only if they have the same value returned by generate-id().

Using a grouping without a key is very slow, because the processor has to recursively process every single Employee node in the document for each and every node in the node set.  If there are 10 Employee nodes, it will process 10 Employee nodes for the first one, 10 for the second, 10 for the third...etc.  Using a key allows us to process all the Employee nodes *once* and thereafter look them up by an index key, rather than an XPath expression.

Regards,
Mike Sharp
I'd like to qualify my statement a bit, because as I re-read it, I see it doesn't quite say what I want.  This key:

<xsl:key name="keyEmployeeID" match="Employee" use="concat(TeamID,EmployeeID)"/>

has *every* Employee node in the document, indexed by BOTH TeamID and EmployeeID, and this key:

<xsl:key name="keyEmployeeID" match="Employee" use="EmployeeID"/>

has the same nodes, but indexed only by EmployeeID.  So consider this XML:

<Employees>
      <Employee>
            <TeamID>1</TeamID>
            <TeamName>Sales</TeamName>
            <TaskID>1</TaskID>
            <Hours>5</Hours>
            <EmployeeID>1</EmployeeID>
            <Name>Bob</Name>
            <Surname>Shibob</Surname>
      </Employee>
      <Employee>
            <TeamID>1</TeamID>
            <TeamName>Sales</TeamName>
            <TaskID>2</TaskID>
            <Hours>4</Hours>
            <EmployeeID>1</EmployeeID>
            <Name>Bob</Name>
            <Surname>Shibob</Surname>
      </Employee>
      <Employee>
            <TeamID>2</TeamID>
            <TeamName>Finance</TeamName>
            <TaskID>5</TaskID>
            <Hours>4</Hours>
            <EmployeeID>1</EmployeeID>
            <Name>Bob</Name>
            <Surname>Shibob</Surname>
      </Employee>
</Employees>

Bob Shibob is in two different Teams.  So if I use only the second key, and I'm grouping, I can only get the first occurance of Bob, where he's in Sales.  In order to get the other nodes, I could use these XPath expressions:

Where EmployeeID = 1:

key('keyName', EmployeeID)   <-- This is a nodeset of Employee nodes where EmployeeID = 1
key('keyName', EmployeeID)[1]   <-- This is a nodeset of only the *first* Employee node where EmployeeID = 1
key('keyName', EmployeeID)[3]   <-- This is a nodeset of only the *third* Employee node where EmployeeID = 1
key('keyName', EmployeeID)[TeamID = 1]   <-- This is a nodeset of all Employee nodes where EmployeeID =1 and TeamID = 1

So, while the <xsl:key> indexes ALL the Employee nodes, I can get access to any particular one.  That's where the Muenchian grouping comes in.   Later when we group, we're looking for the first occurance of an Employee node with a given EmployeeID.  BUT, if the employee appears in more than one Team, we'll never see the nodes for the other team.  That's why I had to add TeamID to the key; I had to be able to get unique occurrances of any node by EmployeeID AND by TeamID.  Modify my example stylesheet, removing the TeamID from the keys, and see what happens.  

This XML:

<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="transform-broken.xslt"?>
<Employees>
      <Employee>
            <TeamID>1</TeamID>
            <TeamName>Sales</TeamName>
            <TaskID>1</TaskID>
            <Hours>5</Hours>
            <EmployeeID>1</EmployeeID>
            <Name>Bob</Name>
            <Surname>Shibob</Surname>
      </Employee>
      <Employee>
            <TeamID>1</TeamID>
            <TeamName>Sales</TeamName>
            <TaskID>2</TaskID>
            <Hours>4</Hours>
            <EmployeeID>1</EmployeeID>
            <Name>Bob</Name>
            <Surname>Shibob</Surname>
      </Employee>
      <Employee>
            <TeamID>1</TeamID>
            <TeamName>Sales</TeamName>
            <TaskID>4</TaskID>
            <Hours>7</Hours>
            <EmployeeID>2</EmployeeID>
            <Name>Sara</Name>
            <Surname>Lee</Surname>
      </Employee>
      <Employee>
            <TeamID>2</TeamID>
            <TeamName>Finance</TeamName>
            <TaskID>5</TaskID>
            <Hours>2</Hours>
            <EmployeeID>3</EmployeeID>
            <Name>John</Name>
            <Surname>Smith</Surname>
      </Employee>
      <Employee>
            <TeamID>2</TeamID>
            <TeamName>Finance</TeamName>
            <TaskID>3</TaskID>
            <Hours>4</Hours>
            <EmployeeID>4</EmployeeID>
            <Name>Penny</Name>
            <Surname>Wise</Surname>
      </Employee>
      <Employee>
            <TeamID>2</TeamID>
            <TeamName>Finance</TeamName>
            <TaskID>5</TaskID>
            <Hours>3</Hours>
            <EmployeeID>4</EmployeeID>
            <Name>Penny</Name>
            <Surname>Wise</Surname>
      </Employee>
      <Employee>
            <TeamID>2</TeamID>
            <TeamName>Finance</TeamName>
            <TaskID>5</TaskID>
            <Hours>4</Hours>
            <EmployeeID>1</EmployeeID>
            <Name>Bob</Name>
            <Surname>Shibob</Surname>
      </Employee>
</Employees>


This XSLT:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="xml"/>
      <!-- Define keys used to group elements -->
      <xsl:key name="keyTeamID" match="Employee" use="TeamID"/>
      <xsl:key name="keyEmployeeID" match="Employee" use="EmployeeID"/>
      <xsl:template match="Employees">
            <table>
                  <!-- Process each Team -->
                  <xsl:for-each select="Employee[generate-id(.) = generate-id(key('keyTeamID', TeamID)[1])]">
                        <!-- Convenience variable to hold the employees for this team.  -->
                        <xsl:variable name="vTeamEmployees" select="/Employees/Employee[TeamName=current()/TeamName]"/>
                        <tr>
                              <td>TEAM: <xsl:value-of select="TeamName"/>
                              </td>
                              <td>HOURS</td>
                        </tr>
                        <!-- Show details for Employees in Team -->
                        <xsl:for-each select="$vTeamEmployees[generate-id(.) = generate-id(key('keyEmployeeID', EmployeeID)[1])]">
                              <tr>
                                    <td>
                                          <xsl:value-of select="Name"/>
                                          <xsl:value-of select="Surname"/>
                                    </td>
                                    <td>
                                          <!-- Show the total hours for the current Employee -->
                                          <xsl:value-of select="sum($vTeamEmployees[EmployeeID=current()/EmployeeID]/Hours)"/>
                                    </td>
                              </tr>
                        </xsl:for-each>
                        <tr>
                              <td>Sub-Total</td>
                              <td>
                                    <!-- Show the total hours for all Employees in the Team -->
                                    <xsl:value-of select="sum($vTeamEmployees/Hours)"/>
                              </td>
                        </tr>
                  </xsl:for-each>
                  <tr>
                        <td>Grand Total</td>
                        <td>
                              <!-- Show Grand Total of hours for all Employees -->
                              <xsl:value-of select="sum(Employee/Hours)"/>
                        </td>
                  </tr>
            </table>
      </xsl:template>
</xsl:stylesheet>


Produces this output:

<?xml version="1.0" encoding="UTF-8"?>
<table>
      <tr>
            <td>TEAM: Sales</td>
            <td>HOURS</td>
      </tr>
      <tr>
            <td>BobShibob</td>
            <td>9</td>
      </tr>
      <tr>
            <td>SaraLee</td>
            <td>7</td>
      </tr>
      <tr>
            <td>Sub-Total</td>
            <td>16</td>
      </tr>
      <tr>
            <td>TEAM: Finance</td>
            <td>HOURS</td>
      </tr>
      <tr>
            <td>JohnSmith</td>
            <td>2</td>
      </tr>
      <tr>
            <td>PennyWise</td>
            <td>7</td>
      </tr>
      <tr>
            <td>Sub-Total</td>
            <td>13</td>
      </tr>
      <tr>
            <td>Grand Total</td>
            <td>29</td>
      </tr>
</table>


Notice that Bob doesn't show up at all in the second team!


Regards,
Mike Sharp
Thanks Mike, thats a great example! I think I should do some more reading, but its made the concept alot clearer.

I think you should add this to your site as the xml isnt too dificult to comprehend and the explanation introduces the use of indexing the nodes.

What i think i'll do next is use some of the keys above against the xml and use a copy-of select to see how the output nodesets differ.

Thanks again

MetalMickey
Hmm...I think I will do that...though it won't be for a couple weeks--too darn busy at the moment.

Regards,
Mike Sharp