Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Adding Filtering to GridViews

5.00/5 (2 votes)
22 Mar 2011CPOL4 min read 24.8K  
How to make a robust GridView with filtering capabilities

Introduction

The purpose of this article is to show how you can make the GridView so robust, it hurts. There is another personal purpose of this article, letting me write my first article. I have wanted to for some time now, but this time I just did it… I had to, so apologies in advance if this is redundant.

The first thing that I realized is that with the internet getting more functional than we ever thought, the need to add to existing controls is a highly useful skill.

The GridView has built-in functionality that covers most of he spectrum. There are also several articles on how to extend the GridView to meet many needs. This article tackles a very basic need, extending header cells.

We will attempt to add a filter control into a Cell in the Header for a GridView. The HeaderCell will preserve the LinkButton of the column name that provides us the clickable sorting function. There will be an addition Select Options control that will allow the user to select a filter on that column, raising an event that applies a filter to the DataSource of the GridView. The plan is to make sure we have a fully functioning sorting GridView, allowing the OnClick events to present a list of filter options for that column. I have selected to pick on the Columns collection and make each column present this functionality. The ColumnFilter control adds a Column control that can be used in the GridView’s Columns collection, adding customized functionality for the Cell. This is basically a BoundField with some extras so that when it is a DataControlCellType.Header, the cell provides this functionality or otherwise it will only provide the DataField bound value. In the example, we start off by creating a GridView with sorting capabilities. There are articles on this so we will quickly create the WebGridView. This WebGridView allows the Page to control what happens during the sorting. A nice extension to the WebGridView allows the method SetColumnSortDirectionControl(expr, direction, ascending, descending) to manage the sorting.

WebGridView.cs

C#
using System;
using System.Collections;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace Custom.UI.WebApplication.WebControls
{
///
/// Models a  derived control that supports the  control.
///
[ToolboxData("<{0}:WebGridView runat="\""server\"><!--{0}:WebGridView-->")]
public class WebGridView : System.Web.UI.WebControls.GridView, IPageableItemContainer
{
    ///
    /// TotalRowCountAvailable event key
    ///
    private static readonly object EventTotalRowCountAvailable = new object();
    public bool AutoFilter { get; set; }
    ///
    /// Creates the control hierarchy used to render
    /// the  control using the specified data source.
    ///
    ///<param name="dataSource" />An  that contains the data source for the  control.
    /// <param name="dataBinding" />true to indicate
    /// that the child controls are bound to data; otherwise, false.
    /// The number of rows created.
    ///
    ///      returns a null .
    /// -or-
    ///  does not implement the  interface and cannot return a .
    /// -or-
    ///  is true and  does not implement the  interface
    ///       and cannot perform data source paging.
    /// -or-
    ///  does not implement the  interface and  is set to false.
    ///
    protected override int CreateChildControls(IEnumerable dataSource, 
                                               bool dataBinding)
    {
        if (null != dataSource)
        {
            int rows = base.CreateChildControls(dataSource, dataBinding);
            if (this.AllowPaging)
            {
                //Determine total number of rows in the datasource
                IEnumerator enumerator = dataSource.GetEnumerator();
                int sourceCount = 0;
                while (enumerator.MoveNext()){sourceCount++;};
                int totalRowCount = dataBinding ? rows : sourceCount;
                // Raise the row count available event
                IPageableItemContainer pageableItemContainer = 
                         this as IPageableItemContainer;
                this.OnTotalRowCountAvailable(
                new PageEventArgs(
                pageableItemContainer.StartRowIndex,
                pageableItemContainer.MaximumRows,
                totalRowCount
                )
                );
                //  Make sure the top and bottom pager rows are not visible
                if (this.TopPagerRow != null)
                {
                    this.TopPagerRow.Visible = false;
                }
                if (this.BottomPagerRow != null)
                {
                    this.BottomPagerRow.Visible = false;
                }
            }
            return rows;
        }
        return 0;
    }
#region IPageableItemContainer Interface
///
/// Sets the properties of a page of data.
///
///<param name="startRowIndex" />The index of the first record on the page.
/// <param name="maximumRows" />The maximum number of items on a single page.
/// <param name="databind" />true to rebind
///     the control after the properties are set; otherwise, false.
    void IPageableItemContainer.SetPageProperties(
    int startRowIndex, int maximumRows, bool databind)
    {
        if (maximumRows == 0)
            return;
        int newPageIndex = (startRowIndex / maximumRows);
        this.PageSize = maximumRows;
        if (this.PageIndex != newPageIndex)
        {
            bool isCanceled = false;
            if (databind)
            {
                //  create the event args and raise the event
                GridViewPageEventArgs args = 
                    new GridViewPageEventArgs(newPageIndex);
                this.OnPageIndexChanging(args);
                isCanceled = args.Cancel;
                newPageIndex = args.NewPageIndex;
            }
            //  if the event wasn't cancelled
            //  go ahead and change the paging values
            if (!isCanceled)
            {
                this.PageIndex = newPageIndex;
                if (databind)
                {
                    this.OnPageIndexChanged(EventArgs.Empty);
                }
            }
            if (databind)
            {
                this.RequiresDataBinding = true;
            }
        }
    }
    ///
    /// Gets the index of the first record that is displayed on a page of data.
    ///
    ///
    ///
    /// The index of the first record that is displayed on a page of data.
    ///
    int IPageableItemContainer.StartRowIndex
    {
        get { return this.PageSize * this.PageIndex; }
    }
    ///
    /// Gets the maximum number of items to display on a single page of data.
    ///
    ///
    ///
    /// The maximum number of items to display on a single page of data.
    ///
    int IPageableItemContainer.MaximumRows
    {
        get { return this.PageSize; }
    }
    ///
    /// Occurs when the data from the data source is made available to the control.
    ///
    event EventHandler IPageableItemContainer.TotalRowCountAvailable
    {
        add { base.Events.AddHandler(
                   WebGridView.EventTotalRowCountAvailable, value); }
        remove { base.Events.RemoveHandler(
                      WebGridView.EventTotalRowCountAvailable, value); }
    }
    ///
    /// Raises the  event.
    ///
    ///<param name="e" />The  instance containing the event data.
    protected virtual void OnTotalRowCountAvailable(PageEventArgs e)
    {
        EventHandler handler =
        (EventHandler)base.Events[WebGridView.EventTotalRowCountAvailable];
        if (handler != null)
        {
            handler(this, e);
        }
    }
#endregion
    }
}

WebGridViewHelper.cs

Adding extension methods to the WebGrid so that sorting is made easy:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI.WebControls;
using Epsilon.WebInquiry.UI.WebApplication.WebControls;
namespace Custom.UI.WebApplication.Support
{
    static public class WebGridViewHelper
    {
        ///
        /// Sets the column sort direction control.
        ///
        ///<param name="sortExpression" />The sort expression.
        ///<param name="gridView" />The grid view.
        static public void SetColumnSortDirectionControl(this WebGridView gridView, 
               string sortExpression, SortDirection sortDirection, 
               string ascCss, string descCss)
        {
            if (sortExpression != null && sortExpression.Length > 0)
            {
                int cellIndex = -1;
                foreach (DataControlField field in gridView.Columns)
                {
                    if (field.SortExpression.Equals(sortExpression))
                    {
                        cellIndex = gridView.Columns.IndexOf(field);
                        break;
                    }
                }
                if (cellIndex > -1)
                {
                    //  this is a header row,set the sort style
                    gridView.HeaderRow.Cells[cellIndex].CssClass =
                       (sortDirection == SortDirection.Ascending ? ascCss : descCss);
                }
            }
        }
    }
}

The WebGridView creates a GridView with the IPageableItemContainer interface to neatly apply paging and sorting. This class is easily implemented and can replace the GridView verbatim.

The next thing that we will create is a Column that can be used in the WebGridView. The GridFilterField class inherits from DataControlField, which defines a column for the GridView. This class can override the DataControlField methods as needed. This example overrides CreateField, InitializeCell, and ExtractValuesFromCell.

C#
namespace Custom.UI.WebApplication.WebControls
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
///
/// GridFilterField creates a new data field for the gridview control.
///
///
/// This control replaces a boundfield in the gridview control.
/// It adds a Column Filter to the gridview that can be used to filter the column.
///
public class GridFilterField: DataControlField
{
#region Properties
    public string DataField
    {
        get
        {
            object dataField = ViewState["DataField"];
            if (null != dataField)
            {
                return dataField.ToString();
            }
            return string.Empty;
        }
        set { this.ViewState["DataField"] = value; }
    }

    public Unit Width { get; set; }
    public Unit Height { get; set; }
    [UrlProperty()]
    public string FilterIconURL { get; set; }
    [UrlProperty()]
    public string InvokeFilterIconURL { get; set; }
#endregion Properties
#region DataControlField Overrides
///
/// When overridden in a derived class, creates an empty -derived object.
///
///
/// An empty -derived object.
///
    protected override DataControlField CreateField()
    {
        return new GridFilterField();
    }
///
/// Adds text or controls to a cell's controls collection.
///
///<param name="cell" />A  that contains the text or controls of the .
///<param name="cellType" />One of the  values.
///<param name="rowState" />One of the  values,
///     specifying the state of the row that contains the .
///<param name="rowIndex" />The index of the row that the  is contained in.
    public override void InitializeCell(
           System.Web.UI.WebControls.DataControlFieldCell cell, 
           System.Web.UI.WebControls.DataControlCellType cellType, 
           System.Web.UI.WebControls.DataControlRowState rowState, int rowIndex)
    {
        base.InitializeCell(cell, cellType, rowState, rowIndex);
        if (cellType == DataControlCellType.Header)
        {
            var link = cell.Controls.OfType();
            ColumnFilter filter = new ColumnFilter();
            // Set Column filter's properties.
            if (!this.Width.IsEmpty)
                filter.Width = this.Width;
            if (!this.Height.IsEmpty)
                filter.Height = this.Height;
            if (!string.IsNullOrEmpty( this.FilterIconURL))
                filter.FilterIconURL = this.FilterIconURL;
            if (!string.IsNullOrEmpty(this.InvokeFilterIconURL))
                filter.InvokeFilterIconURL = this.InvokeFilterIconURL;
            filter.Text = cell.Text;
            filter.HeaderLinkControl = link.FirstOrDefault() as LinkButton;
            // Remove the control's default link button
            // so that the ColumnFilter can render the link.
            cell.Controls.Remove(link.FirstOrDefault());
            cell.Controls.Add(filter);
            //if(!string.IsNullOrEmpty(this.DataField) && this.Visible)
            //        filter.PreRender +=new EventHandler(cell_PreRender);
        }
        if (cellType == DataControlCellType.DataCell)
        {
            if(!string.IsNullOrEmpty(this.DataField) && this.Visible )
                cell.DataBinding += new EventHandler(cell_DataBinding);
        }
    }
    void cell_DataBinding(object sender, EventArgs e)
    {
        if (sender is DataControlFieldCell)
        {
            DataControlFieldCell cell = (DataControlFieldCell)sender;
            cell.Text = DataBinder.GetPropertyValue(
              DataBinder.GetDataItem(cell.NamingContainer), 
              this.DataField ).ToString();
        }
    }
    ///
    /// Extracts the value of the data control field from the
    /// current table cell and adds the value to the specified  collection.
    ///
    ///<param name="dictionary" />An .
    ///<param name="cell" />A  that contains the text or controls of the .
    ///<param name="rowState" />One of the  values.
    ///<param name="includeReadOnly" />true to indicate
    /// that the values of read-only fields
    /// are included in the collection; otherwise, false.
    public override void ExtractValuesFromCell(
      System.Collections.Specialized.IOrderedDictionary dictionary, 
      DataControlFieldCell cell, DataControlRowState rowState, bool includeReadOnly)
    {
        base.ExtractValuesFromCell(dictionary, cell, rowState, includeReadOnly);
        string value = null;
        if (cell.Controls.Count > 0)
            value = ((TextBox)cell.Controls[0]).Text;
        if (dictionary.Contains(this.DataField))
            dictionary[this.DataField] = value;
        else
            dictionary.Add(this.DataField, value);
    }
#endregion DataControlField Overrides
    }
}

The Item in the DataControlField that We Added

ColumnFilter is the actual column that will be doing the work. I have added some JavaScript to expand the node and then coded in the ColumnFilter the necessary support for rendering a basic table with two cells; the top cell has the link button that is created when a GridView tries to sort data and the expandable div that houses the control for the filtering.

C#
namespace Custom.UI.WebApplication.WebControls
{
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Web.UI.HtmlControls;
[DefaultProperty("Text")]
[ToolboxData("<{0}:ColumnFilter runat="\""server\" />")]
public class ColumnFilter : WebControl
{
    ///
    /// Gets or sets the column header text.
    ///
    /// The text.
    public string Text { get; set; }
    ///
    /// Gets or sets the header link control for the column header
    ///
    /// The header link control.
    public LinkButton HeaderLinkControl { get; set; }
    public override Unit Width
    {
        get
        {
            return base.Width;
        }
        set
        {
            base.Width = value;
        }
    }
    public override Unit Height
    {
        get
        {
            return base.Height;
        }
        set
        {
            base.Height = value;
        }
    }
    public string FilterIconURL { get; set; }
    public string InvokeFilterIconURL { get; set; }
    protected override void CreateChildControls()
    {
        //use the Create Child controls to create the Dropdown div feature.
        HtmlGenericControl divcontainer = new HtmlGenericControl("div");
        // Set the div style to display none initially for onclicking.
        divcontainer.Style.Add("display", "none");
        divcontainer.Attributes.Add("class", "filterdiv");
        divcontainer.ID = string.Format("div{0}", 
          this.HeaderLinkControl.CommandArgument);
        divcontainer.Attributes.Add("Name", 
          string.Format("div{0}", 
          this.HeaderLinkControl.CommandArgument));
        if (!string.IsNullOrEmpty(this.InvokeFilterIconURL))
        {
            Image commiticon = new Image();
            commiticon.Attributes.Add("OnClick", 
              string.Format("filterContents('{0}');", 
              this.HeaderLinkControl.CommandArgument));
            commiticon.ImageUrl = this.InvokeFilterIconURL;
            divcontainer.Controls.Add(commiticon);
        }
        Table columnHeader = new Table();
        TableRow title = new TableRow();
        TableCell titlecell = new TableCell();
        TableCell iconcell = new TableCell();
        titlecell.BorderStyle = BorderStyle.None;
        TextBox filtertxt = new TextBox();
        filtertxt.Width = this.Width;
        filtertxt.ID = string.Format("{0}filtertext", 
                       this.HeaderLinkControl.CommandArgument);
        divcontainer.Controls.Add(filtertxt);
        this.HeaderLinkControl.Text = string.Format("{0}", 
                                      this.HeaderLinkControl.Text);
        if (null != this.HeaderLinkControl)
        titlecell.Controls.Add(HeaderLinkControl);
        else
        titlecell.Controls.Add(new Label() { Text = this.Text });
        if (!string.IsNullOrEmpty(this.FilterIconURL))
        {
            Image filtericon = new Image();
            filtericon.ImageUrl = this.FilterIconURL;
            filtericon.Attributes.Add("OnClick", 
              string.Format("ExpandCollapse('{0}');", divcontainer.UniqueID));
            //filtericon.OnClientClick = string.Format(
              "ExpandCollapse('div{0}');", this.HeaderLinkControl.Text);
            iconcell.Controls.Add(filtericon);
            title.Cells.Add(iconcell);
        }
        title.Cells.Add(titlecell);
        //TableRow filterDivRow = new TableRow();
        //TableCell filterDivCell = new TableCell();
        //filterDivCell.ColumnSpan = 2;
        //filterDivCell.Controls.Add(divcontainer);
        //filterDivRow.Cells.Add(filterDivCell);
        // Add the TableRows
        columnHeader.Rows.Add(title);
        //columnHeader.Rows.Add(filterDivRow);
        columnHeader.Width = base.Width;
        columnHeader.Height = base.Height;
        columnHeader.Style.Add(HtmlTextWriterStyle.Padding, "1px");
        this.Controls.Add(columnHeader);
        this.Controls.Add(divcontainer);
        base.CreateChildControls();
    }
}
}

Using these two classes along with the WebGridView now gives me full functionality of a bound column. For example:

I can now add to my code-behind of the page with the WebGridView the following method to filter based on the property and value.

C#
protected void FilterData(IDictionary filterData)
{
    IEnumerable source = this.DataSource as IEnumerable;
    var parameter = Expression.Parameter(typeof(Consumer), "consumer");
    Expression filterexpression = null;
    foreach (string filtername in filterData.Keys)
    {
        var property = typeof(Consumer).GetProperty(
            filtername.Replace("value", string.Empty));
        var propertyAccess = Expression.MakeMemberAccess(parameter, property);
        var equality = Expression.Equal(propertyAccess, 
                       Expression.Constant(filterData[filtername]));
        if (filterexpression == null)
        {
            filterexpression = equality;
        }
        else
        {
            filterexpression = Expression.And(filterexpression, equality);
        }
    }
    Expression> predicate = Expression.Lambda>(filterexpression, parameter);
    Func compiled = predicate.Compile();
    this.DataSource = source.Where(compiled).ToList();
    DataBind();
}

The string replace method is really just for my JavaScript-ing; since I have so much going on in the JavaScript that it functions as the div name so that I can find it and extract the value of the text box. This is important because it is what the ColumnFilter uses when creating the child controls. Otherwise, it should just be the name of the property in simple cases.

One note to mention here is that I have posted to a form some values that represent the text box values and I get them from the Request.Form values. Here is a little snippet that helps me immensely:

C#
this.FilterExpression = Request.Form.Keys.Cast()
   .Where(field => field.Contains("value"))
   .ToDictionary(field => field, field => Request.Form[field]);

That little tidbit will give me all of the Form elements that end in my magical “value” name. So I get all of the text values that were passed into the Form elements.

I have this working and will probably tweak it for Likes and keeping the filtering in state, which is another blog topic all together.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)