Randomizing SharePoint’s Content Query Web Part

It’s not a flying hot dog, but the CQWP is pretty cool

Bart McDonough

August 10, 2011

12 Min Read
ITPro Today logo in a gray background | ITPro Today


Imagination and versatility are a powerful combination. As the parent of a 2-year-old son, I’m reminded of that almost daily. He’s constantly innovating, and almost any object you hand him to play with will suddenly have a range of new uses you never imagined. If you’ve ever seen a hot dog become an airplane or watched an orange fly through a basketball net, you know exactly what I mean.

Although I would never dare compare Microsoft Office SharePoint Server’s Content Query Web Part (CQWP) with a hot dog flying through the air, I can’t ignore the fact that it, too, possesses a wide range of uses. Using a little imagination, I’ve found that the CQWP covers an amazing number of scenarios.

One such case came up on a project I was working on recently. We were building a corporate intranet with SharePoint Server 2010, and the client requested that we randomly display some items on the home page from one of their lists. Technically, it was a little more complicated than that, because there was a notion of active vs. inactive items and some other minor details, but the point is that we needed to randomly choose the items that got displayed. We knew the CQWP would be a good fit for querying the list items and formatting them for display, but we needed a way to randomize it. In this article, I discuss the approach I took to solve the problem, and I explain the reasoning behind some of the design decisions I made.

 

Solving the Problem with XSLT

To get started, I did a quick web search to see if anyone else had already done something similar. I didn’t find much that was useful except a couple of blog posts that solved a similar problem with Extensible Style Language Transformations (XSLT). I say “similar” because they were displaying only a single random item at a time (typically an image). I needed to display a set.

The approach in the blog posts was to create a custom XSLT extension function (similar to the ddwrt:FormatDate function that’s often used to format dates) and register it with the CQWP for use in Extensible Stylesheet Language (XSL). The function was a simple random number generator that returned a value between 1 and the number of query results being iterated in the XSL. When the function was called, the random number it returned was used as an index into the query results, and only that particular result was displayed.

If I had used that approach, registering an XSLT extension function with the CQWP isn’t difficult. Suppose, for example, that I wanted to create my own ToUpper function that let me convert a string to uppercase. First, I’d create a class with my extension function, as Listing 1 shows.

Next, I’d register an instance of that class with the CQWP as an XSLT extension object (i.e., a type of object that extends normal XSLT functionality). To do that, I’d override the ModifyXsltArgumentList() method of the ContentByQueryWebPart class, as Listing 2 shows. Within that method, I’d call AddExtensionObject() on the argList parameter that’s passed in and register my extension object with a custom namespace.

On the XSL side, I could access the extension object’s methods as extension functions by adding a namespace declaration, like so:

xmlns:custom=urn:my-xslt-extensions 

Then, I could convert a string to uppercase by calling the function, like so:

 

As you can see, creating and using custom extension functions in XSLT isn’t all that difficult. And although I could’ve used this approach to generate random numbers and use them in XSL, as in the blog examples I’d seen, this solution wasn’t quite what I was looking for. I was looking for a more generic approach that would work even with the out-of-box XSL files provided by SharePoint. Custom extension functions would mean deploying custom XSL files, and I wanted to avoid doing so if possible—which left me with one other option: Randomly choose query results before rows are passed to the XSL transform for display.

Solving the Problem without XSLT

Solving the problem without XSLT meant intercepting the query results before they were passed to the XSL transform, randomly selecting some of them, and passing only those results to the transformation process. Intercepting query results in the CQWP means using the ProcessDataDelegate property. ProcessDataDelegate is a property on the ContentByQueryWebPart class that allows a method to be assigned that can intercept and process query results before they’re transformed.

I created a new class that inherited from ContentByQueryWebPart and assigned a method to the ProcessDataDelegate property in the constructor. Listing 3 shows the method I created. As you can see in the code, I didn’t bother creating a second DataTable and copying rows into it. I simply removed the rows I didn’t want from the original table and returned whatever was left. The key here was generating multiple, unique random numbers for selecting the rows to return. I wrote the GetUniqueRandomIndexes method to do just that, as Listing 4 shows.

 

Generating Multiple Unique Random Numbers

Generating the random numbers meant solving two problems. First, I had to ensure uniqueness (i.e., that each number was returned only once). Second, I had to make sure the probabilities were kept fair. For example, if my query results had 30 rows, the first number selected should have a 1 in 30 chance of being picked, the second number a 1 in 29 chance, the third a 1 in 28 chance, and so on.

I addressed both problems by starting with a list of available indexes to choose from. If my query had 30 results, for instance, the available indexes would range from 0 to 29. Each time a random number is generated, the number is used as an index into that list. (Remember that the items in the list are themselves indexes into the collection of query results.) After retrieving the item at that index, the item is removed from the list of available indexes (ensuring uniqueness), and the random number generator now has one less number to choose from the next time it’s called (ensuring the probabilities remain fair).

 

When ItemLimit Isn’t Quite Enough

You might have noticed that the code in Listing 4 refers to a DisplayItemLimit property on the Web Part class. The reason I created that property was to offer the user of the Web Part a way to configure how many random items get displayed. At first, you might think the normal ItemLimit property on the Web Part would suffice—but it turns out that’s not the case.

The ItemLimit property limits the number of query results returned. This is fine when using the out-of-box Web Part, because limiting the number of results displayed by limiting what comes back in the query is OK. In my case, however, it wasn’t. I might want 100 items to come back in the query but only want 10 items randomly chosen and displayed out of that 100. I needed a separate property to clarify that distinction.

 

Editing the Web Part in SharePoint

At this point, I had a fully functional randomized CQWP and could’ve called it good. However, there was still one issue to consider. What would happen if an end user chose to edit my Web Part in SharePoint’s UI? How would the user set my new DisplayItemLimit property and select how many random items should be displayed?

The answer was that I’d need to create a custom tool part for editing my property and add it to the Web Part’s tool pane (the panel that appears when the Web Part is in edit mode). As Figure 1 shows, that’s exactly what I did. I created a new tool part called Randomization Settings and provided a text box for setting the DisplayItemLimit property. To add my tool part to the tool pane, I overrode the GetToolParts() method in my Web Part class and inserted my tool part in the second-from-top position.

McDonough136380Fig1_0

Figure 1: Custom tool part

 

You might be wondering, “Why didn’t you just decorate your DisplayItemLimit property with a few attributes and let SharePoint auto-generate the editing interface for you? Then you wouldn’t have needed a custom tool part.” That’s an excellent question, and I’m glad to see you’re paying careful attention. The short answer is that this approach doesn’t work with the CQWP.

For SharePoint to auto-generate editing controls for custom Web Part properties, the Web Part must add an instance of the CustomPropertyToolPart class to its tool pane when in edit mode. Many Web Parts do this, but the CQWP doesn’t. As a result, the only option you’re left with is to build your own tool parts for any custom properties you want users to edit.

 

A Potential Point of Confusion

Now that my custom tool part was done, I found one other issue related to editing. You could argue that this is being extremely nitpicky, but it’s something I thought might be confusing to users, which is a big No-No from a UI design standpoint.

Let’s consider what happens when you edit an out-of-box CQWP in SharePoint. The regular Web Part tool pane appears, and an extra tool part is added at the top containing two categories of settings: Query and Presentation. The Presentation category contains a setting for item limit. Above that text box is a check box labeled Limit the number of items to display.

A user who selects that check box might think he’s doing exactly what it says: limiting the number of items that get displayed. However, as I mentioned earlier, that setting actually limits the number of items returned by the query, not the number of items that get displayed. And because I already added my own custom tool part for specifying how many items get displayed, I decided the best way to tackle this would be to give that preexisting check box a more accurate label.

 

Clearing Up the Confusion

At first, I thought I’d relabel the check box by inheriting from the ContentByQueryToolPart class, which is the class responsible for creating it. However, that class is marked as sealed, which tosses that option out the window. I also considered using jQuery to change the label text on the client side—but I wasn’t really fond of that option, so I kept looking for another way.

Then it dawned on me that I could use ASP.NET’s control hierarchy to do the work. I wrote a method called ReviseBaseCQWPItemLimitSection(), as Listing 5 shows. Listing 6 shows the FindControlRecursive method it uses. Figure 2 shows the resulting check box label. Finally, I overrode the OnPreRender method in my tool part class and called the ReviseBaseCQWPItemLimitSection() method there.

McDonough136380Fig2_0
Figure 2: Relabeling the check box that limits the number of items returned by the query



Done! Now I had a fully functional Web Part and a custom editing experience for it. I’d also removed a potential point of confusion by giving a preexisting setting a more accurate label.

 

A Lesson in Versatility

Developing a randomized CQWP was another good lesson for me in just how versatile this Web Part is. Now, whenever I have another requirement to display a Featured Photos section or something similar, I have this Web Part at my disposal.

This article only scratches the surface of randomizing the CQWP. Depending on your requirements, there are other customizations you could make. For instance, you could add logic that keeps a set of random items on a page for a set amount of time (regardless of page refreshes) before they get swapped out with the next set.

There are lots of possibilities. And although it might not be a hot dog flying through the air, I think the CQWP still exhibits the kind of versatility my 2-year-old son would be proud of.

 

Try It Yourself

If you’d like to see the full source code for the Web Part and try it out yourself, you can download the sample Microsoft Visual Studio solution that accompanies this article. (Click the 136380.zip hotlink near the top of the page.) After you’ve downloaded the code, follow these steps:

  1. Open the solution on a machine with Visual Studio 2010 and SharePoint 2010 installed.

  2. Set the Site URL property on the project to a valid site collection URL.

  3. Compile and deploy the solution.

  4. In the target site collection, activate the Site-scoped feature named Randomized Content Query Web Part under Site Settings, Site Collection Features.

  5. Add the Web Part to a page. The Web Part will be located in the Content Rollup category of the Web Part gallery.

Now that you’ve got your own randomized CQWP to play with, see how inventive you can be!


Listing 1: Creating a Class with the Extension Function
public class XsltExtensions
{
    public string ToUpper(string input)
    {
        input = input ?? "";
        return input.ToUpper();
    }
}


Listing 2: Overriding the ModifyXsltArgumentList() Method
public class CustomContentQueryWebPart : ContentByQueryWebPart
{
    XsltExtensions _xsltExtensions = new XsltExtensions();
    
    protected override void ModifyXsltArgumentList(ArgumentClassWrapper argList)
    {
        base.ModifyXsltArgumentList(argList);
        
        argList.AddExtensionObject("urn:my-xslt-extensions", _xsltExtensions);
    }
}


Listing 3: ProcessQueryResults Method for the ProcessDataDelegate Property
private DataTable ProcessQueryResults(DataTable queryResults)
{
    // Randomly select some rows for display. Filter out the rest.

    int[] indexes = GetUniqueRandomIndexes(queryResults.Rows.Count - 1);

    for (int i = queryResults.Rows.Count - 1; i >= 0; --i)
    {
        if (!indexes.Contains(i))
        {
            queryResults.Rows.RemoveAt(i);
        }
    }

    return queryResults;
}


Listing 4: GetUniqueRandomIndexes Method
// Random number generator (System.Random) as class
// member variable
Random _random = new Random();

private int[] GetUniqueRandomIndexes(int maxIndex)
{
    List indexes = new List();
    IEnumerable allIndexes = Enumerable.Range(0, maxIndex + 1);

    if (DisplayItemLimit == 0 || DisplayItemLimit > (maxIndex + 1))
    {
        indexes.AddRange(allIndexes);
    }
    else
    {
        List remainingIndexes = new List(allIndexes);
        for (int i = 1; i <= DisplayItemLimit; ++i)
        {
            int index = _random.Next(0, remainingIndexes.Count);
            indexes.Add(remainingIndexes[index]);
            remainingIndexes.RemoveAt(index);
        }
    }

    return indexes.ToArray();
}


Listing 5: ReviseBaseCQWPItemLimitSection() Method
private void ReviseBaseCQWPItemLimitSection()
{
    try
    {
        Label label = FindControlRecursive(
            base.ParentToolPane,
            "CBQToolPartlimitNumberOfItemsCheckBoxLabel") as Label;

        if (label != null)
        {
            label.Text = "Limit number of items returned by query";
        }
    }
    catch { }
}


Listing 6: FindControlRecursive Method
private Control FindControlRecursive(Control rootControl, string controlID)
{
    if (rootControl.ID == controlID) return rootControl;

    foreach (Control controlToSearch in rootControl.Controls)
    {
        Control controlToReturn =
            FindControlRecursive(controlToSearch, controlID);
            
        if (controlToReturn != null) return controlToReturn;
    }
    return null;
}

Sign up for the ITPro Today newsletter
Stay on top of the IT universe with commentary, news analysis, how-to's, and tips delivered to your inbox daily.

You May Also Like