Content Query Web Part (CQWP), Cross List Query Nightmare Part 2

Wednesday, 24 October 2007 14:52 by RanjanBanerji

Extending the Content Query Web Part

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 inheriting 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 appropriate 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 elements in those positions?  What happens if Microsoft 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.......

Tags:  
Categories:   SharePoint
Actions:   E-mail | Permalink | Comments (3) | Comment RSSRSS comment feed

Comments

September 28. 2008 05:21

rik

Interesting piece of code, i'm lookinto having this integrated with an "enhanced content query webpart" that also allows paging and setting all additionalfilterfields from the UI.

I have a rather unrelated question: have you ever looked into the missing today() function in calculated fields? We are missing such a function in various situations and someone with your technical insights could have an interesting view on this...

Situation 1:
Say we want to group a series of calendar events in a content by query, grouped in an "outlook" kind of way: today/ tomorrow / wednesday (relative) / thursday / friday / next week.

Situation 2:
We have a large group of members, and want to display all birtydays in the next 30 days using a content by query.

Both shouldn't be to hard, but sharepoint has proven me wrong, and so far we haven't had any success at doing so...

Kind regards,
Rik

rik

September 29. 2008 11:31

RanjanBanerji

Rik,

Not sure about situation 1 but situation 2 should be handled by the CQWP mmodifcation I made.  I guess you will create two filters
something like Birthday > Today AND
Birthday < Today + 30

I set the offset to be a negative number but using the same technique to inject UI you could put a drop down and let the user select the offset as + or -.

Thanks,

Ranjan

RanjanBanerji

June 17. 2010 23:01

trackback

Content Query Web Part (CQWP), Cross List Query Nightmare Part 1

Content Query Web Part (CQWP), Cross List Query Nightmare Part 1

Ranjan Banerji

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
  • Comment
  • Preview
Loading