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
using System;
using System.Collections;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace Custom.UI.WebApplication.WebControls
{
[ToolboxData("<{0}:WebGridView runat="\""server\"><!--{0}:WebGridView-->")]
public class WebGridView : System.Web.UI.WebControls.GridView, IPageableItemContainer
{
private static readonly object EventTotalRowCountAvailable = new object();
public bool AutoFilter { get; set; }
protected override int CreateChildControls(IEnumerable dataSource,
bool dataBinding)
{
if (null != dataSource)
{
int rows = base.CreateChildControls(dataSource, dataBinding);
if (this.AllowPaging)
{
IEnumerator enumerator = dataSource.GetEnumerator();
int sourceCount = 0;
while (enumerator.MoveNext()){sourceCount++;};
int totalRowCount = dataBinding ? rows : sourceCount;
IPageableItemContainer pageableItemContainer =
this as IPageableItemContainer;
this.OnTotalRowCountAvailable(
new PageEventArgs(
pageableItemContainer.StartRowIndex,
pageableItemContainer.MaximumRows,
totalRowCount
)
);
if (this.TopPagerRow != null)
{
this.TopPagerRow.Visible = false;
}
if (this.BottomPagerRow != null)
{
this.BottomPagerRow.Visible = false;
}
}
return rows;
}
return 0;
}
#region IPageableItemContainer Interface
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)
{
GridViewPageEventArgs args =
new GridViewPageEventArgs(newPageIndex);
this.OnPageIndexChanging(args);
isCanceled = args.Cancel;
newPageIndex = args.NewPageIndex;
}
if (!isCanceled)
{
this.PageIndex = newPageIndex;
if (databind)
{
this.OnPageIndexChanged(EventArgs.Empty);
}
}
if (databind)
{
this.RequiresDataBinding = true;
}
}
}
int IPageableItemContainer.StartRowIndex
{
get { return this.PageSize * this.PageIndex; }
}
int IPageableItemContainer.MaximumRows
{
get { return this.PageSize; }
}
event EventHandler IPageableItemContainer.TotalRowCountAvailable
{
add { base.Events.AddHandler(
WebGridView.EventTotalRowCountAvailable, value); }
remove { base.Events.RemoveHandler(
WebGridView.EventTotalRowCountAvailable, value); }
}
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:
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
{
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)
{
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
.
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;
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
protected override DataControlField CreateField()
{
return new GridFilterField();
}
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();
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;
cell.Controls.Remove(link.FirstOrDefault());
cell.Controls.Add(filter);
}
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();
}
}
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.
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
{
public string Text { get; set; }
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()
{
HtmlGenericControl divcontainer = new HtmlGenericControl("div");
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));
"ExpandCollapse('div{0}');", this.HeaderLinkControl.Text);
iconcell.Controls.Add(filtericon);
title.Cells.Add(iconcell);
}
title.Cells.Add(titlecell);
columnHeader.Rows.Add(title);
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.
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:
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.