Solved

Keys

Posted on 2004-10-19
8
219 Views
Last Modified: 2006-11-17
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
0
Comment
Question by:metalmickey
  • 6
  • 2
8 Comments
 
LVL 26

Expert Comment

by:rdcpro
ID: 12350837
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
0
 
LVL 26

Accepted Solution

by:
rdcpro earned 250 total points
ID: 12351218
In fact, his original example won't work if it's possible for an employee to be a member of more than one team.  Let's say you have an administrative assistant that spends half his work week in Finance, and half in Sales.  Or more realistically, instead of Teams, let's say you're working on Projects.  It's very likely some employees will work on more than one project!

To fix this, you must add the TeamID to the keyEmployeeID.  I'll post an updated XSLT below, but I wanted to add another trick to this example that you can use, namely that you can also use keys themselves as a very simple way to aggregate the data, since they are essentially just document indexes.  The XSLT below fixes the one employee in multiple teams bug, and illustrates how you can use a third key to aggregate data. To test, I added John Smith to both teams:


<?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="concat(TeamID,EmployeeID)"/>
      <xsl:key name="keyHours" match="Hours" use="concat(../TeamID, ../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[TeamID=current()/TeamID]"/>
                                    <tr>
                                          <td colspan="4" class="DarkBack">TEAM: <xsl:value-of select="TeamName"/> (<xsl:value-of select="count($vTeamEmployees)"/>)
                                          </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', concat(TeamID,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 using a key to aggregate-->
                                                      <xsl:value-of select="sum(key('keyHours', concat(TeamID, EmployeeID)))"/>
                                                </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>



By the way, using this same approach I was able to create a spectacularly complex report using data in Microsoft's Premier Support Services database, grouping not only by employee and support ticket, but by product line, and then across the table showing columns for each month going back one year at a time.  Each row displayed statistics aggregated in a large number of ways.   The XSLT was huge, but there was very little duplicate code, and best of all, it kept each "piece" of the puzzle in it's own little sandbox, which made it far easier to maintain.

The "key" to it (sorry about the pun) is to think of the xsl:key as a way of defining the page structure, then use that nodeset to filter the document nodes to include or summarize data.  

Regards,
Mike Sharp
0
 
LVL 6

Author Comment

by:metalmickey
ID: 12357912
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
0
 
LVL 26

Expert Comment

by:rdcpro
ID: 12360696
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
0
How your wiki can always stay up-to-date

Quip doubles as a “living” wiki and a project management tool that evolves with your organization. As you finish projects in Quip, the work remains, easily accessible to all team members, new and old.
- Increase transparency
- Onboard new hires faster
- Access from mobile/offline

 
LVL 26

Expert Comment

by:rdcpro
ID: 12360813
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
0
 
LVL 26

Expert Comment

by:rdcpro
ID: 12361121
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
0
 
LVL 6

Author Comment

by:metalmickey
ID: 12367631
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
0
 
LVL 26

Expert Comment

by:rdcpro
ID: 12373436
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
0

Featured Post

Threat Intelligence Starter Resources

Integrating threat intelligence can be challenging, and not all companies are ready. These resources can help you build awareness and prepare for defense.

Join & Write a Comment

Introduction In my previous article (http://www.experts-exchange.com/Microsoft/Development/MS-SQL-Server/SSIS/A_9150-Loading-XML-Using-SSIS.html) I showed you how the XML Source component can be used to load XML files into a SQL Server database, us…
I was working on a PowerPoint add-in the other day and a client asked me "can you implement a feature which processes a chart when it's pasted into a slide from another deck?". It got me wondering how to hook into built-in ribbon events in Office.
Excel styles will make formatting consistent and let you apply and change formatting faster. In this tutorial, you'll learn how to use Excel's built-in styles, how to modify styles, and how to create your own. You'll also learn how to use your custo…
In this tutorial you'll learn about bandwidth monitoring with flows and packet sniffing with our network monitoring solution PRTG Network Monitor (https://www.paessler.com/prtg). If you're interested in additional methods for monitoring bandwidt…

758 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

19 Experts available now in Live!

Get 1:1 Help Now