Archive

Archive for August, 2009

Consuming Filter Web Parts with a Web Part containing a User Control

August 15, 2009 Leave a comment

When wrapping an ASP.NET User Control in a web part, the user control is usually loaded in the CreateChildControls() method as shown below:

protected override void CreateChildControls()
{
  if (!_error)
  {
    try
    {
       base.CreateChildControls();

       // Your code here...
       MyWebUserControl myControl = 
         (MyWebUserControl)Page.LoadControl("~/_controltemplates/MyWebPart/MyWebUserControl.ascx");
       myControl.DataProperty = SomeProcessing();
       this.Controls.Add(myControl);
    }
    catch (Exception ex)
    {
       HandleException(ex);
    }
  }
}

Unfortunately this causes a problem when the embedded user control is to consume filter values coming from a web part connection because connections are evaluated after the CreateChildControls() method is executed. In the example above, when the SomeProcessing() method is executed no filter connections will have been created and therefore no filter values are available.

Therefore a mechanism is required to access the filter values later in the web part life cycle, perform filtering and update the user control with the filtered data.

The most obvious solution is to simply move the loading of the user control until after the connection has been created, for example in the OnPreRender() event handler. However this seems a little too ‘hacky’ to me. If anyone has a better suggestion please leave a comment below.

Notes

  1. Steven Van de Crean lists the order of execution for the ASP.NET web part in this blog post.
Advertisements

Cross-Site Collection Query, Almost

August 13, 2009 4 comments

This is a tale of getting close to one of SharePoint’s holy grails but not quite…

Site collections are the most scalable SharePoint container and they offer lots of advantages over building site heirarchies with layers of sub-sites. However information in one site collection can’t be made visible to another site collection using out-of-the-box SharePoint components. This is a major inconvenience when trying to aggregate content on a portal for example.

Solution (almost)

The solution evolves from first using the SPSiteDataQuery class to run a query upon each site collection and then aggregating the results.

The example below queries all Calendar lists for events within a date range:

// for each site collection
var currentApp = SPContext.Current.Site.WebApplication;
foreach (SPSite site in currentApp.Sites)
{
  var query = new SPSiteDataQuery()
  { 
    RowLimit = 100,
    Lists = @"<Lists ServerTemplate='106' />",
    Webs = "<Webs Scope='SiteCollection' />",
    Query =
        String.Format(
        @"<Where>
            <And>
              <Geq>
                <FieldRef Name='EventDate' />
                <Value Type='DateTime'>{0}</Value>
              </Geq>
              <Leq>
                <FieldRef Name='EndDate' />
                <Value Type='DateTime'>{1}</Value>
              </Leq>
            </And>
          </Where>",
          startDate.ToString("yyyy-MM-dd"), endDate.ToString("yyyy-MM-dd")),
    ViewFields =
        "<FieldRef Name='Title' />
         <FieldRef Name='ID' />
         <FieldRef Name='EventDate' />
         <FieldRef Name='EndDate' />
         <FieldRef Name='Location' />
         <FieldRef Name='Description' />
         <FieldRef Name='fAllDayEvent' />
         <FieldRef Name='fRecurrence' />
         <FieldRef Name='FileRef' />"
  };
  var results = site.RootWeb.GetSiteData(query);

  // aggregate the results
  ...
}

This will work, however it clearly doesn’t scale well as it will query many sites and webs each time it is run.

The next step was to consider the CrossListQueryCache class which provides the ability to cache the results. In reality, apart from introducing the cache, this class doesn’t do much more than wrap the SPSiteDataQuery class and the call to SPWeb.GetSiteData().

Continuing the same example, swap out the SPSiteDataQuery with:

var query = new CrossListQueryInfo
  {
    UseCache = true,
    RowLimit = 100,
    Lists = @"<Lists ServerTemplate='106' />",
    Webs = "<Webs Scope='SiteCollection' />",
    Query =
        String.Format(
        @"<Where>
            <And>
              <Geq>
                <FieldRef Name='EventDate' />
                <Value Type='DateTime'>{0}</Value>
              </Geq>
              <Leq>
                <FieldRef Name='EndDate' />
                <Value Type='DateTime'>{1}</Value>
              </Leq>
            </And>
          </Where>",
          startDate.ToString("yyyy-MM-dd"), endDate.ToString("yyyy-MM-dd")),
    ViewFields =
        "<FieldRef Name='Title' />
         <FieldRef Name='ID' />
         <FieldRef Name='EventDate' />
         <FieldRef Name='EndDate' />
         <FieldRef Name='Location' />
         <FieldRef Name='Description' />
         <FieldRef Name='fAllDayEvent' />
         <FieldRef Name='fRecurrence' />
         <FieldRef Name='FileRef' />"
  };

var cache = new CrossListQueryCache(query);
var results = cache.GetSiteData(site, site.RootWeb.ServerRelativeUrl);

Unfortunately attempting to use the cache for each site collection query causes problems – any attempt to query a site collection beyond the current one results in a error:

There is no Web named "/sites/WebSite". 

This is due to the way that CrossListQueryCache class is written – as revealed by examining Microsoft.SharePoint.Publishing with .NET Reflector. During the execution of the GetSiteData method, a call is made to the getWeb method of the ContentByQueryWebPart:

using (SPWeb web = ContentByQueryWebPart.getWeb(webUrl))
{
    return this.GetSiteData(web);
}

So when the cached query is executed it uses the getWeb method to get the reference to the SPWeb object to run the query against. The problem is that this method uses the context of the calling code, via SPContext, to open the web site specified by the URL:

internal static SPWeb getWeb(string webUrl)
{
    SPSite site = SPContext.Current.Site;
    SPWeb web = null;
    web = site.OpenWeb(webUrl);
    bool isRootWeb = web.IsRootWeb;
    return web;
}

Thus, even though the caller has passed through the correct SPSite to use when opening the SPWeb against which the query is to be run, the CrossListQueryCache ignores this and instead uses the context of the caller.

Close but no cigar…

The net result is that a cross-site collection query is possible using SPSiteDataQuery but clearly this could have a serious performance impact with querying multiple site collections and sub-sites. It would be nice to be able to have the query results cached however this doesn’t seem possible using CrossSiteQueryCache.

It’s somewhat infuriating that a relatively simple change to the CrossListQueryCache class would enable cached cross-site collection queries.