This is something that keeps coming up at work, our clients need to filter the results of large multi-page database queries directly in the grid.
One way you can do this is to use a template field to neatly integrate the textbox in the gridview column header. Adding this functionality to the gridview is relatively straightforward, but not without some quirks. You must also consider that you'll have to replace all columns you wish to filter with a templatefield, but that's usually not too much trouble.

Here's how we declare our columns:
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" CellPadding="4"
GridLines="None">
<FooterStyle Font-Bold="True" ForeColor="White" />
<HeaderStyle HorizontalAlign="Left" />
<Columns>
<asp:TemplateField>
<HeaderTemplate>
<asp:Label runat="server">Title</asp:Label>
<cus:GridHeaderFilter DataField="Title" runat="server"></cus:GridHeaderFilter>
</HeaderTemplate>
<ItemTemplate>
<asp:Label runat="server"><%#Eval("Title")%></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Author">
<HeaderTemplate>
<asp:Label ID="Label1" runat="server">Author</asp:Label>
<cus:GridHeaderFilter DataField="Author" runat="server"></cus:GridHeaderFilter>
</HeaderTemplate>
<ItemTemplate>
<asp:Label runat="server"><%#Eval("Author")%></asp:Label>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
The first step is to create the GridHeaderFilter custom control, which is essentially an asp .net Textbox that will raise the "Filter" event on a text change.
[ToolboxData("<{0}:GridHeaderFilter runat=server>")]
public class GridHeaderFilter : WebControl, INamingContainer
{
...
protected override void CreateChildControls()
{
base.CreateChildControls();
this.tbFilter = new TextBox();
this.tbFilter.AutoPostBack = true;
this.tbFilter.TextChanged += new EventHandler(tbFilter_TextChanged);
this.tbFilter.Width = Unit.Percentage(100);
this.Controls.Add(this.tbFilter);
this.PreRender += new EventHandler(GridHeaderFilter_PreRender);
}
void GridHeaderFilter_PreRender(object sender, EventArgs e)
{
// the datagrid and gridview clear the viewstate of all rows during databinding,
// so1 we can't count on the viewstate to retrieve filter text during postbacks
tbFilter.Text = Page.Request.Form[tbFilter.UniqueID];
}
void tbFilter_TextChanged(object sender, EventArgs e)
{
RaiseBubbleEvent(this, new CommandEventArgs("Filter", new FilterEventArgs(this.dataField, tbFilter.Text)));
}
}
What we're doing is hooking up the textbox to raise a RowCommandEvent when the text in the filter changes. We can then catch it in the RowCommandEvent of the GridView like you would a any other command. A FilterEventArgs is passed as a CommandArgument and contains the DataField that was filtered and the filter value itself.
void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
switch (e.CommandName)
{
case "Filter":
FilterEventArgs fa = e.CommandArgument as FilterEventArgs;
string datafield = fa.DataField;
string filter = fa.Filter;
... (query database with filter expression and rebind grid)
You may have wondered earlier why the following line is required:
tbFilter.Text = Page.Request.Form[tbFilter.UniqueID];
The problem that comes up when we rebind the filtered data, is that the grid also clears the viewstate of its children. Unfortunately, that means the textbox loses the filter text after filtering. But since a Textbox is an html input of type='text' that posts its value on submit, we can simply retrieve the filter text by querying the form value collection with its UniqueID.
(If anyone knows of a way to conserve the header row's viewstate without extending the gridview itself, please tell me how!)
At this stage we know when the user has changed the filter, which filter it was and we have the new filter value in the FilterEventArgs instance. We would be done if we were restricting to a single column filter and the only thing left would be to query the database and bind the filtered data back to the gridview.
But we're aiming a bit higher than that in this example! What we have to do is store all the filters in a structure that will persist them during postbacks. The way I did it was to save to viewstate overriding the page's SaveViewState and LoadViewState methods in charge of (de)serializing viewstate data.
protected System.Collections.Specialized.NameValueCollection filters;
protected override object SaveViewState()
{
Pair p = new Pair(base.SaveViewState(), filters);
return p;
}
protected override void LoadViewState(object savedState)
{
Pair p = (Pair)savedState;
base.LoadViewState(p.First);
if (p.Second != null)
filters = (NameValueCollection)p.Second;
}
That's it! Have a look at the full source code for implementation details.
A later Howto will deal with including the filtering code in a custom FilterableGridView
No comments:
Post a Comment