|

Limiting Group Items in the Content Query WebPart

April 18, 2008 06:52 posted by jwages

The other day I was at a local SharePoint user group meeting, and someone wanted to know how to use a Content Query WebPart with grouping, but only bring back the first X items in each group.   Sure, you can limit the total number of results returned in the UI, but apparently there was no easy way to limit each individual group count.  There was a bit of discussion on how to do this, but it turns out it's a little more tricky than you'd think. 

Solving The Problem

stylelibrary

If you've dealt in detail with the CQW, you know there are a few stylesheets that are used to display the output.  The two primary files located in the Style Library are Header.xsl and ItemsStyle.xsl.  Representing the group header output and the individual item output respectively.  What you may not have dealt with much is the ContentQueryMain.xsl file.  This is the "conductor" file, which is main entry point for the data transformation and it is what calls to the header and item style files.

If you look into the ContentQueryMain file you'll see that there is really only one loop for the dataset and that each individual row is passed to the ItemStyle template, one at a time.  Therefore, position counting doesn't get you anything in this case since all you know is your overall position in the result set, and not your position within a group.  This also rules out using some kind of MOD operation to get only the first X results in a group. 

You also can't use a mock counter variable or parameter in XSL since there is no nested loops setup.  Meaning you can't track anything over the long haul, unless you completely restructure the core XSL of the CQW.  If this were a one-off site, sure you could cut out all unnecessary code and restructure things, but that would be a pain.

The answer lies in using the XSL functions to get the current position in a group by looking backwards in the result set.  Using the XSL function preceding-sibling returns back all the previous nodes at the same level as the current node (basically a nodeset containing all the past nodes at the same level).  Using this function along with an XPath query to limit the results to the current group and a count function you can get the current position within a group.

The only problem with this is that you have to know the attribute name to compare against and the value that you want to compare.  It get's a little confusing, because as you change the group by column in the UI the attribute name changes to reflect this.  Fortunately there is already a variable setup called $Group, which refers to the attribute name of the field you are grouping on. 

The Solution

To actually get this to work is very simple, even in a very generic way.  If you look in the ContentQueryMain.XSL file, within the OuterTemplate.Body section, towards the bottom of the template look for this call (around line 118 or so):

<xsl:call-template name="OuterTemplate.CallItemTemplate">
    <xsl:with-param name="CurPosition" select="$CurPosition" />
</xsl:call-template>
 
This is what passes a row of data over to the ItemStyle templates (eventually), and by default it passes the current position within the overall dataset.  So, the solution to this problem is to calculate the group position in the main Body function, pass it along to the item style functions, and then use that value in an item style to limit the rows that are written out.
 
To start, let's create an item style template and call it LimitTo5.  Open the ItemStyle.XSL file and create a new item style template.  Easiest way is to copy an existing template and change it's name and matching rule. I just copied the Default item style and added a parameter for our passed in position.  I also wrapped the display portion of the template in an IF statement based on the current position.
 
<xsl:template name="LimitTo5" match="Row[@Style='LimitTo5']" mode="itemstyle">
    <xsl:param name="CurPos" />
    <!-- VARIABLE DECLARATIONS LEFT OUT FOR BREVITY -->
    <xsl:if test="$CurPos &lt; 5">
        <!-- WRITE OUT DATA HERE -->
    </xsl:if>
</xsl:template>
 
Save this back into your site, and open the ContentQueryMain.XSL file.  Locate the OuterTemplate.CallItemTemplate function, and look for the last <xsl:otherwise> block.  Add in the parameter to pass along the current position to the individual item style template.
 
<xsl:otherwise>
    <xsl:apply-templates select="." mode="itemstyle">
        <xsl:with-param name="CurPos" select="$CurPosition" />
    </xsl:apply-templates>
</xsl:otherwise>

Now we've got to actually calculate the current position within the group.  Locate the OuterTemplate.Body function, and find the xsl:variable declaration for the CurPosition - around line 78.  We are going to create two new variables: one to hold the name of the current group and one to hold the position within the group.  Right after the CurPosition declaration add the following items.

<xsl:variable name="CurPosition" select="position()" />
<!-- BELOW TWO VARIABLES ARE NEEDED FOR GROUP POSITION CALCULATING -->
<xsl:variable name="GroupName" select="@*[name()=$Group]" />
<xsl:variable name="GroupPosition">
    <xsl:value-of select="count(preceding-sibling::*[@*[$Group]=$GroupName])"/>
</xsl:variable>

Notice that these are declared within the for-each block, meaning their value will change with each iteration through the loop.  The variable $Group is referenced here, and as mentioned above this variable holds the attribute name of the field you are grouping on.  The first variable, GroupName, is set to the text value of the field you are grouping on (i.e. Site 1).  The GroupPosition variable counts how many previous rows there are which have the grouping attribute set to the value of the current row.  Effectively giving you the count within the group.

Lastly we just need to selectively pass the GroupPosition on to our item templates.  Scroll to the end of the OuterTemplate.Body function and find where the OuterTemplate.CallItemTemplate is called.  As you'll see it is already passing the $CurPosition variable on.  We could just change this to pass the $GroupPosition variable, but in order to make this as generic as possible let's only pass the group position when we are actually using an item style that utilizes it.  Create an ItemPosition variable to hold our data:

<xsl:variable name="ItemPosition">
    <xsl:choose>
        <xsl:when test="starts-with(@Style,'LimitTo')">
            <xsl:value-of select="$GroupPosition"/>
        </xsl:when>
        <xsl:otherwise>
            <xsl:value-of select="$CurPosition"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:variable>

Pretty self explanatory here, but basically it checks to see if the item style we are using starts with LimitTo, and if it does pass over the group position instead of the overall position.  You can change this to whatever filter you want, but in this way all the other functionality of the CQW is preserved across all your sites.

Now, just change the OuterTemplate.CallItemTemplate to use the ItemPosition variable.

<xsl:call-template name="OuterTemplate.CallItemTemplate">
    <xsl:with-param name="CurPosition" select="$ItemPosition" />
</xsl:call-template>

CQW-Interface[4] To test this out, configure a CQW on a page and turn grouping on.  Then set the Item Style to our LimitTo5 style and watch it go.  Groups will display items up to the defined limit in the item style.  To further is on, you can edit the item style to add a More link.

<xsl:if test="$CurPos=4">
    <a href="">More...</a>
</xsl:if>

Another good option would be for some way to be able to pass in a value for how many items you wanted to limit to.  I briefly looked over the existing fields in the CQW, but didn't see a real good candidate for this.  Maybe in the extended attributes, but I'll save that for another day.