On a recent project, we wanted to make extensive use of the MOSS Content Query Web Part’s capability to query the instances of a content type on our SharePoint site and then use custom XSL transformations to present this data to the users as HTML. The problem that we ran into was that some of the data that we needed to present came from sources other than the content type instances (e.g., configuration files, web part properties, web services). To overcome this problem while still taking advantage of the web part’s built-in capabilities, we needed a way to modify the data used by the web part after the data has been collected via the query but before it has been converted to HTML by the XSLT. After a little research and experimentation, we discovered that Microsoft provides an easy mechanism to do just this via the ProcessDataDelegate property of the ContentByQueryWebPart class.
ContentByQueryWebPart is the name of the class that implements the web part (notice the “By” in the class name). This class can be found in the Microsoft.SharePoint.Publishing.WebControls namespace of the Microsoft.SharePoint.Publishing.dll assembly. To make use of the ProcessDataDelegate property of this class, we need to extend the class and override its OnInit method.
public class EnhancedCQWP : Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart
{
protected override void OnInit(EventArgs e)
{
this.ProcessDataDelegate += new ProcessData(modifyData);
base.OnInit(e);
}
Inside OnInit, we will wire up the ProcessDataDelegate property to a new instance of ProcessData that executes a method called modifyData. ProcessData requires a method that takes a DataTable as a parameter and has a DataTable as its return value. The signature of our modifyData method is shown below as an example.
private DataTable modifyData(DataTable dt)
Code in the ContentByQueryWebPart calls the methods that have been wired up to this delegate after executing the query to retrive the data but before performing the XSL transformation. The data from the query is passed into our method via the DataTable parameter. The DataTable passed out of our method is then passed along to the XSLT. This gives us the opportunity to make whatever modifications to the data are necessary.
In the example below, modifyData loops through the rows in the data table and makes some trivial updates to existing Title and Description columns. It also deletes the row with Title == “Article2”. The data being queried consists of three Article pages.
private DataTable modifyData(DataTable dt)
{
List<DataRow> rowsToDelete = new List<DataRow>();
foreach (DataRow row in dt.Rows)
{
try
{
//Change the description of Article1
if (row[TITLE_COLUMN_NAME].ToString() == "Article1")
{
row[DESCRIPTION_COLUMN_NAME] += " This is the text that I have appended
to the description.";
}
//Change the title of Article3
if (row[TITLE_COLUMN_NAME].ToString() == "Article3")
{
row[TITLE_COLUMN_NAME] += " This is the text that I have appended to the
title.";
}
//Save the the rows to be deleted
if (row[TITLE_COLUMN_NAME].ToString() == "Article2")
{
rowsToDelete.Add(row);
}
}
catch (Exception e)
{
File.AppendAllText("C:\temp\test.txt", e.Message + " " + e.StackTrace);
}
}
//Delete the rows
foreach (DataRow row in rowsToDelete)
{
row.Delete();
}
return dt;
}
This screen shot shows the results of using the enhanced web part and the base web part to query the same data.
A few things to note about the code:
1) I have defined a set of constants to assist with accessing the data table columns by name. The ContentByQueryWebPart renames these columns for you before sending the data to the XSLT; however, this renaming takes place after the ProcessDataDelegate is executed.
private const string TITLE_COLUMN_NAME = "{fa564e0f-0c70-4ab9-b863-0177e6ddd247}";
private const string FILEREF_COLUMN_NAME = "{94f89715-e097-4e8b-ba79-ea02aa8b7adb}";
private const string ID_COLUMN_NAME = "{1d22ea11-1e32-424e-89ab-9fedbadb6ce1}";
private const string MODIFIED_COLUMN_NAME = "{28cf69c5-fa48-462a-b5cd-27b6f9d2bd5f}";
private const string AUTHOR_COLUMN_NAME = "{1df5e554-ec7e-46a6-901d-d85a3881cb18}";
private const string EDITOR_COLUMN_NAME = "{d31655d1-1d5b-4511-95a1-7a09e9b75bf2}";
private const string CREATED_COLUMN_NAME = "{8c06beca-0777-48f7-91c7-6da68bc07b69}";
private const string DESCRIPTION_COLUMN_NAME = "Comments";
2) I had issues trying to delete a row while iterating through the DataRowCollection. Doing so seems to throw off the enumerator. To overcome this, I track the rows to be deleted and then delete them after looping through the entire collection.
3) The example does not show how to add a new column to the DataTable. However, this can be easily accomplished by creating a new DataColumn instance and adding it to the columns collection via DataTable.Columns.Add(). The data in the added column can be accessed in the XSLT just like any other column.
The code in modifyData in our example is an extremely trivial example of what is possible. This code could do anything needed to retrieve data to be inserted into the DataTable or manipulate existing data in the table.
In a future blog post, I will show how to add custom properties to the web part to support yet another source of data for the DataTable.