|
SharePoint 2007 Diary
Content Query Web Part (CQWP), Cross
List Query Nightmare Part 2
Extending the Content Query Web Part
Developer Notes Series
In
Part 1 I talked about how the CQWP creators overlooked
what I felt is a critical feature, i.e., the ability to create a
query using relative dates as a filter criteria. Microsoft
proposes some workarounds for this but they all come up
short.
In
Part 2 I will talk about modifying the CQWP.
I have seen several blog
posts on the CQWP and several that create extended versions of
it. However, I could not find one that handled relative
dates. This was a bit of a challenge for me as I had never
built a web part before and of course I barely had any experience
with SharePoint and its object model.
None the less the
process started. It did not take long to determine that the
ContentByQueryWebPart resides in the
Microsoft.SharePoint.Publishing namesapce and is a public
class. This was good news. This implied I could inherit
from this class, make some modifications to cater for relative dates
and move on.
Not so simple.
What I did not know is that a web part is configured via another
object, i.e., a ToolPart. So if I wanted to modify the UI of
the CQWP to enter a offset value on a date query I needed to work on
it ToolPart which is the ContentByQueryToolPart. But
here starts my problem. You see, the ContentByQueryToolPart is
a sealed class. So while I can create a class by inherting
from the ContentByQueryWebPart I cannot easily create a few
overrides and be done by inherting from
ContentByQueryToolPart.
The
ContentByQueryWebPart is open but the ContentByQueryToolPart is sealed
Why
did Microsoft choose to seal the
ContentByQueryToolPart? I have no
clue. But it sure created some significant
inconvenience. I was now left with two
options:
1. Create a web
part by inheriting from
the ContentByQueryWebPart and create a tool
part.
Override the following method to now use your new ToolPart.
public override ToolPart[] GetToolParts() {
return new ToolPart[] { new MyToolPart() /*ContentByQueryToolPart()*/, new WebPartToolPart() };
}
But
this option is not easy. Using reflector one can see that
the ContentByQueryToolPart is a large complex
class that makes many calls to internal methods. So copying
its code to create your own or to re-invent the wheel could be time
consuming.
2. Find a way to
hack the code. LOL. Yes, this is the option I went
with. A bad, dangerous option. More on that later.
The hack is to find a way to modify the ContentByQueryToolPart UI without the
need to create a new ToolPart. The idea is to
get the control tree of the ToolPart and then inject new UI elements
into it. Then find a way to load and extract data from the
injected UI elements and send the data to the apprpriate CQWP
code that will then do what is needed. Cross List queries use
CAML as input to execute the queries. CAML and the Cross
List Query technology is fully capable of handling relative dates, i.e., an expression like
[Today]-7.
[Guid( "50cc2520-8afc-47dd-w20e-d4567f89j7fm" )]
public class MyExtendedCQWP : ContentByQueryWebPart {
#region Data
///
/// Reference to a TextBox that we will inject as Offset days for filter 1
///
TextBox _textBoxOffset1;
///
/// Reference to a TextBox that we will inject as Offset days for filter 1
///
TextBox _textBoxOffset2;
///
/// Reference to a TextBox that we will inject as Offset days for filter 1
///
TextBox _textBoxOffset3;
///
/// Reference to the sealed ContentByQueryToolPart used by the ContentByQueryWebPart
/// We get a reference to it in the override GetToolParts below.
///
ContentByQueryToolPart _contentByQueryToolPart = null;
private string _dateOffset1 = string.Empty;
private string _dateOffset2 = string.Empty;
private string _dateOffset3 = string.Empty;
#endregion Data
///
/// Constructor
///
public MyExtendedCQWP() {
this.ExportMode = WebPartExportMode.All;
this.Title = "My Extended Content Query Web Part";
}
#region Overrides
///
/// Override of WebPart GetToolParts method
///
///
public override ToolPart[] GetToolParts() {
ToolPart[] retVal = base.GetToolParts();
_contentByQueryToolPart = ( ContentByQueryToolPart )retVal[ 0 ];
_contentByQueryToolPart.Init += new EventHandler( ContentByQueryToolPart_Init );
_contentByQueryToolPart.Load += new EventHandler( ContentByQueryToolPart_Load );
return retVal;
}
///
/// Override of the ContentByQuery GetXPathNavigator method. This is where
/// modify the FilterValue to account for the offset
///
///
///
protected override XPathNavigator GetXPathNavigator( string viewPath ) {
if( FilterValue1 == "[Today]" ) {
FilterValue1 = "[Today]-" + _dateOffset1;
}
if( FilterValue2 == "[Today]" ) {
FilterValue2 = "[Today]-" + _dateOffset2;
}
if( FilterValue3 == "[Today]" ) {
FilterValue3 = "[Today]-" + _dateOffset3;
}
XPathNavigator x = base.GetXPathNavigator( viewPath );
return x;
}
protected override void Render( HtmlTextWriter writer ) {
base.Render( writer );
// TODO: add custom rendering code here.
// writer.Write("Output HTML");
}
#endregion Overrides
#region Events
///
/// Handler for when the ContentByQueryToolPart is loaded. This way
/// we can data back from the injected UI to local fields.
///
///
///
void ContentByQueryToolPart_Load( object sender, EventArgs e ) {
if( ( ( ContentByQueryToolPart )sender ).Page.IsPostBack ) {
_dateOffset1 = _textBoxOffset1.Text;
_dateOffset2 = _textBoxOffset2.Text;
_dateOffset3 = _textBoxOffset3.Text;
}
else {
}
}
///
/// The OnInit event for the ToolPart. Checks to see if a Today Radio button is on the form
/// If so It creates a corresponding Offset TextBox. There can be upto 3 Today Radio buttons.
/// Then we inject the offset TextBox and a label into the ToolParts control tree.
///
///
///
void ContentByQueryToolPart_Init( object sender, EventArgs e ) {
if( _contentByQueryToolPart != null ) {
Control radio = FindControlRecursive( _contentByQueryToolPart, "CBQToolPartfilter1DateTodayRadioButton" );
if( radio != null ) {
radio.Parent.Parent.Controls[ 1 ].Controls.Add( new LiteralControl( " - " ) );
_textBoxOffset1 = new TextBox();
_textBoxOffset1.Width = new Unit( 20 );
_textBoxOffset1.ID = "textBoxOffset1";
_textBoxOffset1.Text = _dateOffset1;
radio.Parent.Parent.Controls[ 1 ].Controls.Add( _textBoxOffset1 );
radio.Parent.Parent.Controls[ 1 ].Controls.Add( new LiteralControl( " Offset Days" ) );
}
radio = FindControlRecursive( _contentByQueryToolPart, "CBQToolPartfilter2DateTodayRadioButton" );
if( radio != null ) {
radio.Parent.Parent.Controls[ 1 ].Controls.Add( new LiteralControl( " - " ) );
_textBoxOffset2 = new TextBox();
_textBoxOffset2.Width = new Unit( 20 );
_textBoxOffset2.ID = "textBoxOffset2";
_textBoxOffset2.Text = _dateOffset2;
radio.Parent.Parent.Controls[ 1 ].Controls.Add( _textBoxOffset2 );
radio.Parent.Parent.Controls[ 1 ].Controls.Add( new LiteralControl( " Offset Days" ) );
}
radio = FindControlRecursive( _contentByQueryToolPart, "CBQToolPartfilter3DateTodayRadioButton" );
if( radio != null ) {
radio.Parent.Parent.Controls[ 1 ].Controls.Add( new LiteralControl( " - " ) );
_textBoxOffset3 = new TextBox();
_textBoxOffset3.Width = new Unit( 20 );
_textBoxOffset3.ID = "textBoxOffset3";
_textBoxOffset3.Text = _dateOffset3;
radio.Parent.Parent.Controls[ 1 ].Controls.Add( _textBoxOffset3 );
radio.Parent.Parent.Controls[ 1 ].Controls.Add( new LiteralControl( " Offset Days" ) );
}
}
}
#endregion Events
#region Properties
///
/// Gets or sets the offset date for a Date filter if [Today] was chosen
///
public string DateOffset1 {
get { return _dateOffset1; }
set { _dateOffset1 = value; }
}
#endregion Properties
#region Private Methods
private Control FindControlRecursive( Control control, string ID ) {
Control retVal = null;
if( control.ID == ID ) {
retVal = control;
}
else {
foreach( Control childControl in control.Controls ) {
Control targetChild = FindControlRecursive( childControl, ID );
if( targetChild != null ) {
retVal = targetChild;
break;
}
}
}
return retVal;
}
#endregion Private Methods
}
The
code above is what your web part code should look like. This
is the result when you selct a
site columm of type Date in any of the
three filters:

WARNING!!!!
DO NOT DO THIS OR USE EXTREME CAUTION
So now that I have shown
you how to inject UI controls into the ContentByQueryToolPart and
hijack its functionality why am I throwing this warning? Quite
simple. If Microsoft wakes up one day and realizes that perhaps a richly
featured querying tool such as the Content Query Web Part should have the
ability to create filters based on relative dates, the code above will get
hosed. Because the code above is not overriding any of the features of
the ContentByQueryToolPart. We are forcibly injecting UI elements.
What happens when Microsoft places their own elemnts in those poistions?
What happens if Micorosft changes their code? Since the
ContentByQueryToolPart is a sealed class I guess they don't expect people doing
stuff with it.
Why did I do the
above? I really needed a short term solution until such time Microsoft
wakes up and makes a better Content Query Web Part.
For now my performance
issues are being taken care of by filtering down the data I get by setting a
filter to get me data only from the last 7 days. Just as I thought I am
done with SharePoint and I can go back to good old coding I was assigned to yet
another SharePoint 2007 mystery. Oh! and it was once again related to the
ContentQuery Web Part. You can just imagine my joy. More on that in
Part 3.......
|