Tuesday, March 30, 2010

Building a Better SPGridView

Sometimes with SharePoint customizations, you run into a scenario where you want to do something that is “just like this OOTB thing, but a little bit different.” Sometimes, that turns out to be trivial. Sometimes it turns out to be a fair amount of work to get what you want. SPGridView is one the latter – sounds great, but takes more work than I’d like to be at feature-parity with the less generic components.

Recently, I had two different projects that wanted something that looked like a ListViewWebpart, but had a more complex data source than a simple view on a list (both involved fetching data from multiple sites within the site collection and joining it to a couple of other lists). Seems simple enough, right? Fetch the data needed from all the source lists (and/or cross-site queries via SPSiteDataQuery) into custom objects, join them together via LINQ-to-Objects, stick the results in a DataTable, display it with SPGridView, and off to the next task, right? Well, yes. It does look right, but sorting and filtering don’t work. Even when you add ObjectDataSource to the mix, there are issues with sorting and filtering getting in each other’s way.

After a bit more digging online, I found a few sources that pointed me in the right direction of what I’d need to do to make it all work:

With these, plus a bit of tinkering, I got it to work like my first client wanted. However, I had to do a lot more work than I wanted to make it happen – until the last piece was in place, it wouldn’t work quite right. Two weeks later, I had another client ask me for almost the same thing (obviously, the data involved was quite a bit different, but the basic idea was the same – custom data fed into a table that looks like a normal SharePoint one). Since I’m a heavy opponent of “reuse by copy and paste”, I thought I’d separate out the “fixes” to the SPGridView from the particulars of this implementation and build a control I could use in the future when this kind of scenario came up.

In my mind, the developer wanting to use this control should only have to provide the information relevant to the problem domain (data, columns) and leave the implementation details (wiring sorting/filtering to work and work together) up to the control. To that end, I present SmartSPGridView:


    1 using System;
    2 using System.Collections.Generic;
    3 using System.Text;
    4 using Microsoft.SharePoint.WebControls;
    5 using System.Web.UI.WebControls;
    6 using System.Data;
    7 using System.Web.UI;
    8 
    9 namespace SPGridDemo
   10 {
   11     /// <summary>
   12     /// Wraps up everything needed for automatic sorting and filtering.
   13     /// Heavily based on:
   14     ///   http://www.reversealchemy.net/2009/05/01/building-a-spgridview-control-part-1-introducing-the-spgridview/
   15     ///   http://www.reversealchemy.net/2009/05/24/building-a-spgridview-control-part-2-filtering/
   16     /// Triggering binding is left up to the parent control -- call GridView.DataBind() -- GetData event will be raised for you to supply the data.
   17     /// Also http://vspug.com/bobsbonanza/2007/07/02/filtering-with-spgridview/
   18     /// </summary>
   19     public class SmartSPGridView : WebControl, INamingContainer
   20     {


   21         public SmartSPGridView()
   22         {
   23             // initialize the ObjectDataSource and SPGridView, wire them together and hook in the needed event handlers
   24             DataSource = new ObjectDataSource()
   25             {
   26                 SelectMethod = "SelectData",
   27                 TypeName = this.GetType().AssemblyQualifiedName,
   28                 ID="DS",
   29             };
   30             GridView = new SPGridView()
   31             {
   32                 AllowSorting = true,
   33                 AllowFiltering = true,
   34                 AutoGenerateColumns = false,
   35                 FilteredDataSourcePropertyName = "FilterExpression",
   36                 FilteredDataSourcePropertyFormat = "{1} = '{0}'",
   37                 ID="view",
   38             };
   39             GridView.DataSourceID = DataSource.ID;
   40             DataSource.ObjectCreating += new ObjectDataSourceObjectEventHandler(DataSource_ObjectCreating);
   41             DataSource.Filtering += new ObjectDataSourceFilteringEventHandler(DataSource_Filtering);
   42             GridView.Sorting += new GridViewSortEventHandler(GridView_Sorting);
   43             GridView.RowDataBound += new GridViewRowEventHandler(GridView_RowDataBound);
   44         }
   45 
   46         public SPGridView GridView { get; private set; }
   47         public ObjectDataSource DataSource { get; private set; }
   48 


   49         /// <summary>
   50         /// Add filter header pieces
   51         /// </summary>
   52         void GridView_RowDataBound(object sender, GridViewRowEventArgs e)
   53         {
   54             if (sender == null || e.Row.RowType != DataControlRowType.Header)
   55             {
   56                 return;
   57             }
   58 
   59             SPGridView grid = sender as SPGridView;
   60 
   61             if (String.IsNullOrEmpty(grid.FilterFieldName))
   62             {
   63                 return;
   64             }
   65 
   66             // Show icon on filtered column
   67             for (int i = 0; i < grid.Columns.Count; i++)
   68             {
   69                 DataControlField field = grid.Columns[i];
   70 
   71                 if (field.SortExpression == grid.FilterFieldName)
   72                 {
   73                     Image filterIcon = new Image();
   74                     filterIcon.ImageUrl = "/_layouts/images/filter.gif";
   75                     filterIcon.Style[HtmlTextWriterStyle.MarginLeft] = "2px";
   76 
   77                     // If we simply add the image to the header cell it will
   78                     // be placed in front of the title, which is not how it
   79                     // looks in standard SharePoint. We fix this by the code
   80                     // below.
   81                     Literal headerText = new Literal();
   82                     headerText.Text = field.HeaderText;
   83 
   84                     PlaceHolder panel = new PlaceHolder();
   85                     panel.Controls.Add(headerText);
   86                     panel.Controls.Add(filterIcon);
   87 
   88                     e.Row.Cells[i].Controls[0].Controls.Add(panel);
   89 
   90                     break;
   91                 }
   92             }
   93         }
   94 


   95         void GridView_Sorting(object sender, GridViewSortEventArgs e)
   96         {
   97             // sorting loses the FilterExpression, so we have to restore it
   98             if (ViewState["FilterExpression"] != null)
   99             {
  100                 DataSource.FilterExpression = (string)ViewState["FilterExpression"];
  101             }
  102         }
  103 
  104         void DataSource_Filtering(object sender, ObjectDataSourceFilteringEventArgs e)
  105         {
  106             // save the filter expression built when we add a filter, so we can restore it when sort blows it away
  107             ViewState["FilterExpression"] = ((ObjectDataSourceView)sender).FilterExpression;
  108         }
  109 
  110         protected override void LoadViewState(object savedState)
  111         {
  112             base.LoadViewState(savedState);
  113 
  114             // clear the saved filter so we don't restore it if the user sorts
  115             if (Context.Request.Form["__EVENTARGUMENT"] != null &&
  116                  Context.Request.Form["__EVENTARGUMENT"].EndsWith("__ClearFilter__"))
  117             {
  118                 // Clear FilterExpression
  119                 ViewState.Remove("FilterExpression");
  120             }
  121         }
  122 


  123         void DataSource_ObjectCreating(object sender, ObjectDataSourceEventArgs e)
  124         {
  125             // called by the ObjectDataSource to create an instance of this object
  126             e.ObjectInstance = this;
  127         }
  128 
  129         protected override void CreateChildControls()
  130         {
  131             // add the ObjectDataSource and SPGridView as children
  132             base.CreateChildControls();
  133             this.Controls.Add(DataSource);
  134             this.Controls.Add(GridView);
  135         }
  136 
  137         protected override void OnLoad(EventArgs e)
  138         {
  139             base.OnLoad(e);
  140             this.EnsureChildControls();
  141         }
  142 
  143         public DataTable SelectData()
  144         {
  145             var e = new GetDataEventArgs();
  146             OnGetData(e);
  147             return e.DataTable;
  148         }
  149 


  150         #region GetData event+args
  151         public class GetDataEventArgs : EventArgs
  152         {
  153             public DataTable DataTable { get; set; }
  154         }
  155         public event EventHandler<GetDataEventArgs> GetData;
  156         protected virtual void OnGetData(GetDataEventArgs e)
  157         {
  158             EventHandler<GetDataEventArgs> eh = GetData;
  159             if (null != eh)
  160             {
  161                 eh(this, e);
  162             }
  163         }
  164         #endregion
  165     }
  166 }

Basically, it’s a UserControl with an ObjectDataSource and SPGridView wired together and with the events needed to get full sorting/filtering working already in place. To use this, all you have to do in your webpart is:

  • Create an instance of SmartSPGridView
  • Hook the GetData