自动过滤 GridView 控件
标准的GridView控件的组件替换,可在标题行中添加过滤器,且无需更改页面上的代码。
引言
我接触ASP.NET只有几年时间,之前是在Classic ASP背景下工作的。我一直很需要一个GridView
控件,能够对列进行过滤。我曾寻找一个简单且免费的控件来满足我的需求,但大多数找到的都是用户手动在标题行或页脚行中放置过滤器,并编写后台代码来处理过滤。我最终也这样做了,但总觉得应该有更简单的方法,不必为我制作的每个页面都重写后台代码,或者为了获得过滤功能而修改每个页面上的GridView
本身。
如果您宁愿直接将控件集成到您自己的项目中,请下载Bin.zip,将DLL放入您项目的bin文件夹,添加对DLL的引用,在您的aspx页面上注册它,并将您的GridView
替换为AutoFilterGridView
。
背景
我一直在重用相同的代码,在太多的ASP.NET页面上为GridView
的标题行添加过滤。这确实有效,但我厌倦了每次想要过滤器时都要重写后台代码。我最终决定使用我现有的一些后台代码、一些新代码,并创建一个新的GridView
控件,我可以简单地将其拖放到页面上就能工作,或者替换页面上的Microsoft GridView
控件,而无需任何其他特殊编码即可为列添加过滤功能。然后我意识到标准过滤器有时是一个缺点,于是我决定更进一步。我意识到对于日期/时间、数字和布尔类型需要最小/最大范围,因此我添加了它们。最终,我为GridView
做了一个简单的替换,当被告知时就会添加过滤器。不需要额外的编码,只需用新的控件替换GridView
。我称之为“AutoFilterGridView
”。我已用SQL Server 2005和2008测试了该控件。请注意,这是一个基本的组件,旨在满足我当时的需求。如果您决定使用它,仍有很大的改进和个性化空间。
Using the Code
为了创建该控件,我继承了**System.Web.UI.WebControls.GridView
**,并添加了一个名为“AutoFilterGridView.vb”的类,然后创建了一个将成为新组件的Shell。
Imports System.Text.RegularExpressions
Imports System.Web.UI.HtmlControls
<Assembly: TagPrefix("MyCustomControl.CustomsControls", "asp")>
<ToolboxData("<{0}:AutoFilterGridView runat=""server""></{0}:AutoFilterGridView>")> _
Partial Public Class AutoFilterGridView
Inherits GridView
End Class
控件还需要一个位置来存储有关它将过滤的字段的信息。为了快速制作组件,我选择将信息存储在一个private
类中,然后将这个类存储在ViewState
中。我意识到这可能不是存储信息的最佳方式,但您完全可以随意重写这个组件。
'used to hold information needed for filtering the fields
' needs to be serializable so we can store it in the viewstate
<Serializable()> _
Private Class FilterInfo
Public Name As String
Public PlaceHolder As String
Public DataFieldType As System.Type
Public DataFieldName As String
Public [Operator] As String
End Class
Private Filters As New List(Of FilterInfo)
接下来,我需要一种通过属性与组件进行通信的方式。我想要一种控制是否启用过滤器、放置应用过滤器按钮和移除过滤器按钮的位置,以及是否希望控件提供对过滤器字段的基本验证。验证是为了防止用户尝试用“Frank
”过滤日期,或用“@@#!
”过滤数字。这些属性将是:
IncludeFilters
:True
表示添加过滤器,False
表示不添加。FilterButtonsColumnIndex
:告诉GridView
将应用和清除按钮放置在哪一列的标题行。ClientCalidateFilters
:True
表示自动验证过滤器,False
表示不验证(这样您就可以在页面上自行处理)。
<Category("Behavior")> _
<Description("Add filters the gridview.")> _
<DefaultValue(False)> _
Public Property IncludeFilters() As Boolean
Get
If String.IsNullOrEmpty(ViewState("IncludeFilters")) Then
Return False
Else
Return DirectCast(ViewState("IncludeFilters"), Boolean)
End If
End Get
Set(ByVal Value As Boolean)
ViewState("IncludeFilters") = Value
End Set
End Property
<Category("Behavior")> _
<Description("Add filter button to which column position (0=first empty)?")> _
<DefaultValue(-1)> _
Public Property FilterButtonsColumnIndex() As Integer
Get
If String.IsNullOrEmpty(ViewState("FilterButtonsColumnIndex")) Then
Return 0
Else
Return DirectCast(ViewState("FilterButtonsColumnIndex"), Integer)
End If
End Get
Set(ByVal Value As Integer)
ViewState("FilterButtonsColumnIndex") = Value
End Set
End Property
<Category("Behavior")> _
<Description("Basic client validation on filters")> _
<DefaultValue(True)> _
Public Property ClientValidateFilters() As Boolean
Get
If String.IsNullOrEmpty(ViewState("ClientValidateFilters")) Then
Return True
Else
Return DirectCast(ViewState("ClientValidateFilters"), Boolean)
End If
End Get
Set(ByVal Value As Boolean)
ViewState("ClientValidateFilters") = Value
End Set
End Property
显然,我们无法在没有一些客户端代码的情况下进行基本的客户端验证。我知道我的一些页面会包含JQuery,而有些则不会,所以我编写了所需的客户端脚本来独立处理这两种情况。我意识到这并非必需,因为编写成不使用JQuery就足够了,但我希望向人们展示如何处理这两种情况。我不会详细介绍这段代码,因为我相信您能够读懂它。我将展示的是我如何将文件包含在组件中。
' Adds the needed js file for validation and clearing the filters
Protected Overrides Sub OnPreRender(e As EventArgs)
MyBase.OnPreRender(e)
Dim resourceName As String = "MyCustomControls.AutoFilterGridView.js"
Dim cs As ClientScriptManager = Me.Page.ClientScript
cs.RegisterClientScriptResource(GetType(MyCustomControls.AutoFilterGridView), resourceName)
End Sub
现在我们进入主要部分。当控件DataBound
(数据绑定)并且*如果我们*希望在标题行中添加过滤器时,我们就需要做一些工作。DataBound
事件是创建过滤器控件以及在ViewState
中存储字段数据信息的地方。
Private Sub AutoFilterGridView_DataBound(sender As Object, e As System.EventArgs) Handles Me.DataBound
' if the control is not told to add filters leave
If Not IncludeFilters Then Return
If Me.Controls.Count > 0 Then
If Not AddFilterHeader() Then Return
End If
' store the filter list in the viewstate
Using sw As New IO.StringWriter()
Dim los = New LosFormatter
los.Serialize(sw, Filters)
ViewState("CustomGridFilters") = sw.GetStringBuilder().ToString()
End Using
Return
End Sub
AddFilterHeader
例程用于遍历gridview
的列,以确定其中的数据类型,创建列过滤器,并将列映射到过滤器列表中。对于每一列,只有三种可能的操作。
- 添加过滤器字段
- 添加相关的过滤器按钮
- 什么都不做
例程首先检查是否需要添加过滤器字段。这会发生在*如果*该列没有通过前面添加的属性指定用于存放过滤器按钮,标题可见,列可见,并且列中的字段是或与BoundField
控件相关。如果该列将获得过滤器,则会调用MakeFilterCell
函数。
如果该列的标题行不接收过滤器,则例程检查该列是否被指定用于接收过滤器按钮。如果是,则添加按钮并设置一个标志,以便不再添加按钮。
如果该列既不接收过滤器也不接收按钮,则创建一个空单元格。
Private Function AddFilterHeader() As Boolean
' get a view of the table
Dim myTable = DirectCast(Me.Controls(0), Table)
Dim myNewRow = New GridViewRow(0, -1, DataControlRowType.Header, DataControlRowState.Normal)
Dim boolFilterDropped As Boolean = False
Filters.Clear()
' Get a view of the data columns
Dim columns As DataColumnCollection
If Not IsNothing(Me.DataSource) AndAlso Me.DataSource.GetType() Is GetType(DataTable) Then
columns = DirectCast(Me.DataSource, DataTable).Columns
ElseIf Not IsNothing(Me.DataSourceObject) AndAlso Me.DataSourceObject.GetType() Is GetType(SqlDataSource) Then
columns = CType(CType(Me.DataSourceObject, SqlDataSource).Select(DataSourceSelectArguments.Empty), DataView).Table.Columns
Else
Return False
End If
'For each column, process it
For x = 1 To Me.Columns.Count
With Me.Columns(x - 1)
If (FilterButtonsColumnIndex <> x) AndAlso _
(.ShowHeader AndAlso .Visible AndAlso GetType(BoundField).IsAssignableFrom(.GetType()) _
AndAlso Not String.IsNullOrEmpty(CType(Me.Columns(x - 1), BoundField).DataField)) Then
Using tc As New TableHeaderCell
AddFilterControls(tc, columns(CType(Me.Columns(x - 1), _
BoundField).DataField.ToString).DataType, CType(Me.Columns(x - 1), _
BoundField).DataField.ToString, .GetType())
tc.CssClass = "filterHeader"
End Using
ElseIf FilterButtonsColumnIndex = x OrElse (FilterButtonsColumnIndex = 0 _
AndAlso .Visible And Not boolFilterDropped) Then
Using tc As New TableHeaderCell
tc.CssClass = "filterButtons"
tc.Controls.Add(New Button() With {.ID = "btnApplyFilters", _
.CommandName = "ApplyFilters", _
.Text = "Filter", .CssClass = "filterButton"})
Using b As New Button
b.ID = "btnClearFilters"
b.CommandName = "ClearFilters"
b.Text = "Clear"
b.CssClass = "filterButton"
b.Attributes.Add("onclick", "ClearAllFilters()")
tc.Controls.Add(b)
End Using
myNewRow.Cells.Add(tc)
End Using
boolFilterDropped = True
ElseIf .Visible Then ' just make an empty cell
myNewRow.Cells.Add(New TableHeaderCell())
End If
End With
Next
myTable.Rows.AddAt(0, myNewRow)
Return True
End Sub
AddFilterControls
例程是创建过滤器并将其添加到标题单元格的地方。为了决定需要哪种类型的过滤器,例程需要查看列中BoundField
与DataField
类型关联的DataFieldType
,然后按需处理它们。用于正确过滤此字段数据的*信息*也将存储在filters
列表中以备后续使用。
它处理的第一种类型是Boolean
。由于该组件允许布尔数据作为复选框或其他类型,我们必须检查列中使用的控件。如果它是一个复选框,那么将使用一个三态复选框来进行过滤(选中、未选中、不确定)。
复选框的状态将由我们之前加载的JavaScript代码处理。
复选框还会被赋予一个id
、name
、data-indeterminate
属性、如果之前被选中则赋予checked
属性,然后是一个class
,以便我们可以在客户端按需设置样式。
Private Sub AddFilterControls(ByRef hc As TableHeaderCell, DataFieldType As System.Type, _
DataFieldName As String, BoundFieldType As System.Type)
' Based on the datatype we will need to make different controls and set the values
Select Case Type.GetTypeCode(DataFieldType)
Case TypeCode.Boolean
If BoundFieldType Is GetType(CheckBoxField) Then
' create a tristate checkbox
Using i As New HtmlGenericControl("input")
i.Attributes.Add("id", "filter1_" & DataFieldName)
i.Attributes.Add("name", i.Attributes("id"))
i.Attributes.Add("type", "checkbox")
i.Attributes.Add("data-indeterminate", _
String.IsNullOrEmpty(If(Page.Request(i.Attributes("name")), String.Empty)))
If String.Compare(If(Page.Request(i.Attributes("name")), _
String.Empty), "True", True) = 0 Then
i.Attributes.Add("checked", String.Compare_
(If(Page.Request(i.Attributes("name")), String.Empty), "True", True) = 0)
End If
i.Attributes.Add("class", "autoFilter tri " & DataFieldType.Name.ToLower)
hc.Controls.Add(i)
Filters.Add(New FilterInfo() With {.Name = i.Attributes("name"), _
.DataFieldType = DataFieldType, .DataFieldName = DataFieldName, .Operator = "="})
End Using
如果Boolean
不是checkbox
类型,那么我们将使其成为一个select
,带有true
、false
和空选项。
该select
会被赋予一个id
、name
和一个class
,以便我们可以在客户端按需设置样式。如果需要,其中一个选项将设置为selected
。
Else
' create a true/false/any dropdownlist
Using i As New HtmlGenericControl("select")
i.Attributes.Add("id", "filter1_" & DataFieldName)
i.Attributes.Add("name", i.Attributes("id"))
Using o As New HtmlGenericControl("option")
o.Attributes.Add("value", "")
o.InnerText = ""
If (If(Page.Request(i.Attributes("name")), String.Empty)) = o.Attributes("value") Then
o.Attributes.Add("selected", "selected")
End If
i.Controls.Add(o)
End Using
Using o As New HtmlGenericControl("option")
o.Attributes.Add("value", "false")
o.InnerText = "False"
If (If(Page.Request(i.Attributes("name")), String.Empty)) = o.Attributes("value") Then
o.Attributes.Add("selected", "selected")
End If
i.Controls.Add(o)
End Using
Using o As New HtmlGenericControl("option")
o.Attributes.Add("value", "true")
o.InnerText = "True"
If (If(Page.Request(i.Attributes("name")), String.Empty)) = o.Attributes("value") Then
o.Attributes.Add("selected", "selected")
End If
i.Controls.Add(o)
End Using
i.Attributes.Add("class", "autoFilter " & DataFieldType.Name.ToLower)
hc.Controls.Add(i)
Filters.Add(New FilterInfo() With {.Name = i.Attributes("name"), _
.DataFieldType = DataFieldType, .DataFieldName = DataFieldName, .Operator = "="})
End Using
End If
例程处理的下一个数据类型是数字类型。由于数字字段提供了一个范围用于过滤,因此会放置2个输入字段。第一个是最小值,第二个是最大值。
这两个input
字段将被赋予属性:id
、name
、class
、placeholder
(以便用户了解该字段的作用)、maxlength
,如果设置了ClientValidateFilters
,则会添加一个onblur
事件调用来进行基本验证。
Case TypeCode.Byte, TypeCode.Decimal, TypeCode.Double, TypeCode.Int16, _
TypeCode.Int32, TypeCode.Int64, TypeCode.SByte, TypeCode.Single, TypeCode.UInt16, TypeCode.UInt32, TypeCode.UInt64
' This is a range control, add min then max
Dim mm As String() = {"min", "max", ">=", "<="}
For x = 1 To 2
Using i As New HtmlGenericControl("input")
i.Attributes.Add("id", "filter" & x.ToString & "_" & DataFieldName)
i.Attributes.Add("name", i.Attributes("id"))
i.Attributes.Add("placeholder", mm(x - 1))
If Type.GetTypeCode(DataFieldType) = TypeCode.Byte Then
i.Attributes.Add("maxlength", 4)
Else
i.Attributes.Add("maxlength", 20)
End If
i.Attributes.Add("class", "autoFilter " & mm(x - 1) & _
"Value numericValue " & DataFieldType.Name.ToLower)
If ClientValidateFilters Then
Select Case Type.GetTypeCode(DataFieldType)
Case TypeCode.Byte, TypeCode.Int16, TypeCode.Int32, TypeCode.Int64, TypeCode.SByte
i.Attributes.Add("onblur", "ValidateAutoFilter(this,/^([\-+]?\d+)?$/)")
Case TypeCode.UInt16, TypeCode.UInt32, TypeCode.UInt64
i.Attributes.Add("onblur", "ValidateAutoFilter(this,/^(\d+)?$/)")
Case TypeCode.Decimal, TypeCode.Double, TypeCode.Single
i.Attributes.Add("onblur", "ValidateAutoFilter(this,/^([-+]?[0-9]*\.?[0-9]+)?$/)")
End Select
End If
i.Attributes.Add("value", If(Page.Request(i.Attributes("id")), String.Empty))
hc.Controls.Add(i)
Filters.Add(New FilterInfo() With {.Name = i.Attributes("id"), _
.DataFieldType = DataFieldType, .DataFieldName = DataFieldName, .Operator = mm(x + 1), _
.PlaceHolder = i.Attributes("placeholder")})
End Using
Next
例程处理的下一个数据类型是日期/时间类型。由于这些字段类型提供了一个范围用于过滤,因此会放置2个输入字段。第一个是最小值,第二个是最大值。
这两个input
字段将被赋予属性:id
、name
、class
、placeholder
(以便用户了解该字段的作用)、maxlength
,如果设置了ClientValidateFilters
,则会添加一个onblur
事件调用来进行基本验证。
Case TypeCode.DateTime
' This is a range control, add min then max
Dim mm As String() = {"min", "max", ">=", "<="}
For x = 1 To 2
Using i As New HtmlGenericControl("input")
i.Attributes.Add("id", "filter" & x.ToString & "_" & DataFieldName)
i.Attributes.Add("name", i.Attributes("id"))
i.Attributes.Add("placeholder", mm(x - 1))
i.Attributes.Add("maxlength", 22)
i.Attributes.Add("class", "autoFilter " _
& mm(x - 1) & "Value " & DataFieldType.Name.ToLower)
If ClientValidateFilters Then
i.Attributes.Add("onblur", "ValidateAutoFilter_
(this, /^((0?[1-9]|1[012])[- /.](0?[1-9]|[12][0-9]|3[01])[- /.](19|20)[0-9]{2}_
( ((2[0-3]|[0-1]?[0-9]):[0-5][0-9](:[0-5][0-9])?|(1[0-2]|[1-9]):[0-5][0-9]_
(:[0-5][0-9])?( (am|pm))?))?)?$/i)")
End If
i.Attributes.Add("value", If(Page.Request(i.Attributes("id")), String.Empty))
hc.Controls.Add(i)
Filters.Add(New FilterInfo() With {.Name = i.Attributes("id"), _
.DataFieldType = DataFieldType, .DataFieldName = DataFieldName, _
.Operator = mm(x + 1), .PlaceHolder = i.Attributes("placeholder")})
End Using
Next
该例程将剩余的类型作为文本处理。这些类型只提供一个input
。
该input
字段然后会被赋予属性:id
、name
、class
、placeholder
(以便用户了解该字段的作用)和maxlength
。
Case Else
Using i As New HtmlGenericControl("input")
i.Attributes.Add("name", "filter_" & DataFieldName)
i.Attributes.Add("id", "filter_" & DataFieldName)
i.Attributes.Add("placeholder", "contains")
If DataFieldType.Name = "Char" Then
i.Attributes.Add("maxlength", 1)
Else
i.Attributes.Add("maxlength", 255)
End If
i.Attributes.Add("class", "autoFilter textValue " & DataFieldType.Name.ToLower)
i.Attributes.Add("value", If(Page.Request(i.Attributes("id")), String.Empty))
hc.Controls.Add(i)
Filters.Add(New FilterInfo() With {.Name = i.Attributes("id"), _
.DataFieldType = DataFieldType, .DataFieldName = DataFieldName, _
.Operator = "LIKE", .PlaceHolder = i.Attributes("placeholder")})
End Using
End Select
End Sub
下一个重要的逻辑分支发生在页面加载时。这是数据将被过滤的地方。如果控件设置为包含过滤功能,并且Page.IsPostBack
为true
,则调用过滤例程。
Protected Sub Page_Load(sender As Object, e As System.EventArgs) Handles Me.Load
If IncludeFilters AndAlso Page.IsPostBack Then
FilterTheData()
End If
End Sub
过滤功能相当基础,只需决定如何应用过滤器。这里唯一的特殊代码是针对日期过滤器,并且仅在最大值*不*包含时间的情况下。由于最大值是包含性的,并且没有时间假定为午夜,我们必须通过加一天来调整该值。对于我们从网页上获得的每个有值的过滤器,都会创建一个FormParameter
并添加到SqlDataSource
的FilterParameters
中。然后,将要使用的过滤器表达式添加到字符串列表中。最后,构建过滤器表达式列表并将其分配给SqlDataSource.FilterExpression
,然后对数据源和组件调用.DataBind
。
Private Sub FilterTheData()
' retrieve the latest filter information from the viewstate
Filters = (New LosFormatter()).Deserialize(ViewState("CustomGridFilters"))
Using ds As SqlDataSource = CType(Me.DataSourceObject, SqlDataSource)
Dim FilterExpressions As New List(Of String)()
With ds
' remove parameters and recreate the needed ones
.FilterParameters.Clear()
' cycle through each filterable column set in the filters list
For Each filter As FilterInfo In Filters
' if there is a request value for the column
'(that is not equal to the placeholder) then add the filter to the datasource
If Not String.IsNullOrEmpty(If(Page.Request(filter.Name), String.Empty)) _
AndAlso Page.Request(filter.Name) <> If(filter.PlaceHolder, String.Empty) Then
Dim p As New FormParameter(filter.Name, filter.Name)
With p
.Type = System.Type.GetTypeCode(filter.DataFieldType)
.DefaultValue = Page.Request(filter.Name)
' If this is a datetime filter and the max value,
' we need to make an adjustment if there is no time.
' Since the filter should be =< the date and a date only
' is treated as midnight, add a dat to the value
If .Type = TypeCode.DateTime AndAlso Regex.IsMatch(filter.Name, "^filter2_") _
AndAlso Regex.IsMatch(.DefaultValue, "^((0?[1-9]|1[012])[- /.]_
(0?[1-9]|[12][0-9]|3[01])[- /.](19|20)[0-9]{2})?$") Then
.DefaultValue = DateAdd(DateInterval.Second, -1, _
DateAdd(DateInterval.Day, 1, DateTime.Parse(.DefaultValue)))
End If
End With
.FilterParameters.Add(p)
' create each expression for the parameter
Select Case System.Type.GetTypeCode(filter.DataFieldType)
Case TypeCode.Boolean
FilterExpressions.Add(filter.DataFieldName & _
filter.Operator & " '{" & (.FilterParameters.Count - 1) & "}'")
Case TypeCode.Byte, TypeCode.Decimal, TypeCode.Double, TypeCode.Int16, _
TypeCode.Int32, TypeCode.Int64, TypeCode.SByte, TypeCode.Single, _
TypeCode.UInt16, TypeCode.UInt32, TypeCode.UInt64
FilterExpressions.Add(filter.DataFieldName & filter.Operator & _
"{" & (.FilterParameters.Count - 1) & "}")
Case TypeCode.DateTime
FilterExpressions.Add(filter.DataFieldName & filter.Operator & _
"#{" & (.FilterParameters.Count - 1) & "}#")
Case Else
FilterExpressions.Add(filter.DataFieldName & _
" " _& filter.Operator & " '%{" & _
(.FilterParameters.Count - 1) & "}%'")
End Select
End If
Next
' convert filter expressions into one expression and assign it to the filterexpression
.FilterExpression = [String].Join(" AND ", FilterExpressions.ToArray())
.DataBind()
End With
Me.DataBind()
End Using
End Sub
我忘记提到的变化
根据Simon在下面留下的消息,我认为我应该解释控件图像中的“编辑”按钮和过滤器按钮。 “编辑”列用于编辑与该行关联的记录。后台代码接收记录ID,您可以根据需要进行处理。它有一个额外的好处,就是留下一个空标题单元格,我让控件将此单元格用于过滤器按钮。如果您不想要GridView
中的命令列,*或者*只是没有一个空的标题区域来让控件放置过滤器按钮,您可以将AutoFilterGridView
的FilterButtonsColumnIndex
设置为-1
(一个不存在的列),这样控件就不会放置按钮。然后,在页面的某个地方,您自己添加2个按钮。它们甚至不必是ASP.NET按钮。您可以将其放在gridview
的后面或前面,
<input class="filterButton" type="submit" value="Filter"> <input class="filterButton" onclick="ClearAllFilters();" type="submit" value="Clear">
它们都能正常工作。