Tuesday, 8 July 2014

Building a Better SPGridView C#

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:
     using System;     using System.Collections.Generic;     using System.Text;     using Microsoft.SharePoint.WebControls;     using System.Web.UI.WebControls;     using System.Data;     using System.Web.UI;          namespace SPGridDemo    {         /// <summary>        /// Wraps up everything needed for automatic sorting and filtering.           /// </summary>         public class SmartSPGridView : WebControl, INamingContainer        {
            public SmartSPGridView()             {                 // initialize the ObjectDataSource and SPGridView, wire them together and hook in the needed event handlers                 DataSource = new ObjectDataSource()                 {                     SelectMethod = "SelectData",                     TypeName = this.GetType().AssemblyQualifiedName,                     ID="DS",                 };                 GridView = new SPGridView()                 {                    AllowSorting = true,                     AllowFiltering = true,                     AutoGenerateColumns = false,                     FilteredDataSourcePropertyName = "FilterExpression",                     FilteredDataSourcePropertyFormat = "{1} = '{0}'",                     ID="view",                 };                 GridView.DataSourceID = DataSource.ID;                 DataSource.ObjectCreating += new ObjectDataSourceObjectEventHandler(DataSource_ObjectCreating);                 DataSource.Filtering += new ObjectDataSourceFilteringEventHandler(DataSource_Filtering);                 GridView.Sorting += new GridViewSortEventHandler(GridView_Sorting);                 GridView.RowDataBound += new GridViewRowEventHandler(GridView_RowDataBound);             }                  public SPGridView GridView { get; private set; }             public ObjectDataSource DataSource { get; private set; }     
           /// <summary>            /// Add filter header pieces             /// </summary>             void GridView_RowDataBound(object sender, GridViewRowEventArgs e)             {                 if (sender == null || e.Row.RowType != DataControlRowType.Header)                 {                     return;                 }                      SPGridView grid = sender as SPGridView;                     if (String.IsNullOrEmpty(grid.FilterFieldName))                 {                     return;                 }                      // Show icon on filtered column                 for (int i = 0; i < grid.Columns.Count; i++)                 {                     DataControlField field = grid.Columns[i];                          if (field.SortExpression == grid.FilterFieldName)                     {                         Image filterIcon = new Image();                         filterIcon.ImageUrl = "/_layouts/images/filter.gif";                        filterIcon.Style[HtmlTextWriterStyle.MarginLeft] = "2px";                              // If we simply add the image to the header cell it will                         // be placed in front of the title, which is not how it                         // looks in standard SharePoint. We fix this by the code                         // below.                         Literal headerText = new Literal();                         headerText.Text = field.HeaderText;                              PlaceHolder panel = new PlaceHolder();                         panel.Controls.Add(headerText);                         panel.Controls.Add(filterIcon);                              e.Row.Cells[i].Controls[0].Controls.Add(panel);                              break;                     }                 }             }     
            void GridView_Sorting(object sender, GridViewSortEventArgs e)             {                 // sorting loses the FilterExpression, so we have to restore it                 if (ViewState["FilterExpression"] != null)                 {                    DataSource.FilterExpression = (string)ViewState["FilterExpression"];                }            }                void DataSource_Filtering(object sender, ObjectDataSourceFilteringEventArgs e)            {                // save the filter expression built when we add a filter, so we can restore it when sort blows it away                ViewState["FilterExpression"] = ((ObjectDataSourceView)sender).FilterExpression;            }               protected override void LoadViewState(object savedState)            {                base.LoadViewState(savedState);                   // clear the saved filter so we don't restore it if the user sorts                if (Context.Request.Form["__EVENTARGUMENT"] != null &&                    Context.Request.Form["__EVENTARGUMENT"].EndsWith("__ClearFilter__"))                {                    // Clear FilterExpression                    ViewState.Remove("FilterExpression");                }            }    
           void DataSource_ObjectCreating(object sender, ObjectDataSourceEventArgs e)            {                // called by the ObjectDataSource to create an instance of this object                e.ObjectInstance = this;           }                protected override void CreateChildControls()            {                // add the ObjectDataSource and SPGridView as children               base.CreateChildControls();                this.Controls.Add(DataSource);                this.Controls.Add(GridView);            }                protected override void OnLoad(EventArgs e)            {                base.OnLoad(e);                this.EnsureChildControls();            }                public DataTable SelectData()            {                var e = new GetDataEventArgs();               OnGetData(e);                return e.DataTable;            }    
           #region GetData event+args            public class GetDataEventArgs : EventArgs            {                public DataTable DataTable { get; set; }            }            public event EventHandler<GetDataEventArgs> GetData;            protected virtual void OnGetData(GetDataEventArgs e)            {                EventHandler<GetDataEventArgs> eh = GetData;               if (null != eh)                {                    eh(this, e);                }            }            #endregion        }    }

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 event and supply the DataTable
  • Call DataBind() at the appropriate time (OnPreRender works well)
  • Configure and add the view to your webpart (usually in CreateChildControls)
    • Add the columns
    • Populate the FilterDataFields property
    • Add the view to the webpart’s Controls collection
ObjectDataSource makes sorting work, FilterDataFields makes filtering work, and several event handlers keep them from tripping over each other. And, I don’t have to keep fighting the same battle every time I need this kind of solution for a project. Now, I’m off to pick another fight with Sharepoint.
The downloadable version of the code is linked below.

No comments:

Post a Comment