Had a client request that turned into rolling my own filter/paging/sorting. I've done this so many times before, but not much in the last five years, as it has become a more or less 'built in' feature in most framework user controls. I have never kept my own library of code for this, because in each case the specific use was customized and the result was quite spaghetti like and nothing I was proud of.
But this time, I think I generalized it enough, and plus, I was able to use a predicate pattern I've used before and so I wanted to write this up for future reference.
Oh, the client codebase is in VB. My first time using VB since college. I'd prefer not to use it again either, but at least the .net libraries are the same.
Remember from previous efforts the state issues, so careful attention to what steps we do at each specific step throughout page lifecycle...
We're using aspnet ListView control, with templates and the aspnet PagerControl, which has an eccentricity where you have to databind after Page_Load, usually in Page_PreRender, so we GetData in PageLoad and save it in a page property without binding yet. Also in Page_Load we bind all of our filter list controls.
Here's the Page_Load's GetData. Notice we actually databind a ListView here but that one is the hidden one. We use a hidden one that is unpaged for the google map pushpins.
Public Sub GetData()
MyCarFeed = New CarFeed(Me.objDP.GetListItems(Me.ModuleID, Me.objCore.Language.CurrentLanguage, "sort").Tables("cars"))
Dim filtertext As String = Me.txtFilter.Text.ToLower()
Filtered = MyCarFeed.Cars.Where(Function(c) _
(String.IsNullOrEmpty(filtertext) OrElse String.Concat( _
c.Description, _
c.Subtitle, _
c.Requirements, _
c.CompanyInformation, _
c.ContactInfo, _
c.Name).IndexOf(filtertext) > -1)). _
Where(ColorFilter()). _
Where(SpecialFilter()). _
Where(AgeFilter()). _
Where(SizeFilter()). _
OrderByDescending(Function(c) c.DateFrom)
' while we are here
Me.LvHiddenForMap.DataSource = Filtered
Me.LvHiddenForMap.DataBind()
End Sub
in the Page_PreRender we call BindPagedList... As long as I can bind again in prerender the PageControl takes care of paging automatically.
Private Sub BindPagedList()
If _sorted = False Then
Sort(ViewState("SortExpression"))
End If
Me.lv.DataSource = If(Filtered Is Nothing, Filtered, Filtered.ToArray())
Me.lv.DataBind()
End Sub
Oh another detail for the finicky DataPager Control, in our case, because we are using it in a template, so we can't add an event to it in markup, we call this method on init:
Protected Sub SetPageSize(ByVal sender As Object, ByVal e As System.EventArgs) ' handles DataPager Init... but it's a templated control so we have to hook it up in html event
If (Not Me.CurrentCarsPageSize Is Nothing) Then
CType(sender, DataPager).PageSize = Me.CurrentCarsPageSize
End If
End Sub
So if we are posting back on a sort then we call Sort here. It's the same Sub we call on the Sorting event:
Protected Sub Sorting(ByVal sender As Object, ByVal e As ListViewSortEventArgs)
Sort(e.SortExpression)
End Sub
And the sort keeps a couple vars in viewstate
Private Sub Sort(ByVal sortexpression As String)
If _pageclicked And Not sortexpression Is Nothing Then
' just rebind
If ViewState("SortOrder") Is Nothing Then
Filtered = Filtered.OrderBy(Function(c As Car) GetCarSortProperty(sortexpression, c))
Else
Filtered = Filtered.OrderByDescending(Function(c As Car) GetCarSortProperty(sortexpression, c))
End If
Else
If Not sortexpression Is Nothing Then
If ViewState("SortExpression") Is Nothing OrElse Not ViewState("SortExpression").Equals(sortexpression) Then
' first click (on this header e.g. new sort)
Filtered = Filtered.OrderBy(Function(c As Car) GetCarSortProperty(sortexpression, c))
SetArrowImages(sortexpression)
ViewState("SortOrder") = Nothing
SetFirstPage()
Else
If ViewState("SortOrder") Is Nothing Then
' second click
Filtered = Filtered.OrderByDescending(Function(c As Car) GetCarSortProperty(sortexpression, c))
SetArrowImages(sortexpression, False)
ViewState("SortOrder") = "Desc"
Else
' third click- reset
sortexpression = Nothing
SetArrowImages(sortexpression)
ViewState("SortOrder") = Nothing
End If
End If
End If
ViewState("SortExpression") = sortexpression
_sorted = True
End If
End Sub
The GetCarSortProperty is nothing more than checking which sort (we are lucky here we only have two columns)
Private Function GetCarSortProperty(ByVal sort As String, ByVal c As Car) As Object
Return IIf(sort.Equals("Name"), c.GetType().GetProperty("Name").GetValue(c, Nothing), IIf(sort.Equals("Location"), c.GetType().GetProperty("Location").GetValue(c, Nothing), Nothing))
End Function
And of course the obligatory SetArrowImages
Private Sub SetArrowImages(ByVal sortexpression As String, Optional ByVal ascending As Boolean = True)
Dim Asc As String = "~/_css/cars/icn_arrow_up.png"
Dim Desc As String = "~/_css/cars/icn_arrow_down.png"
Dim imgSortName As Image = New Image()
Dim imgSortLocation As Image = New Image()
If Not lv.FindControl("imgSortName") Is Nothing Then
imgSortName = CType(lv.FindControl("imgSortName"), Image)
imgSortLocation = CType(lv.FindControl("imgSortLocation"), Image)
End If
If Not sortexpression Is Nothing Then
imgSortName.ImageUrl = IIf(sortexpression.Equals("Name"), IIf(ascending, Asc, Desc), Nothing)
imgSortLocation.ImageUrl = IIf(sortexpression.Equals("Location"), IIf(ascending, Asc, Desc), Nothing)
imgSortName.Visible = sortexpression.Equals("Name")
imgSortLocation.Visible = sortexpression.Equals("Location")
Else ' for third click
imgSortName.Visible = False
imgSortLocation.Visible = False
End If
End Sub
the cool parts are A) that we are generating the filter checkboxes dynamically, and B) the predicates...
Here's one of the categories of filter, called Colors, used in the GetData() at the top of this post
Where(ColorFilter()). _
And here's the function
Private Function ColorFilter() As Func(Of Car, Boolean)
' replicating PredicateBuilder...
Dim t As Predicate(Of Car) = Function() True
Dim f As Predicate(Of Car) = Function() False
Dim cbl As List(Of CheckBox) = GetCheckBoxList(Me.lvColors)
For Each cb As CheckBox In cbl.Where(Function(c As CheckBox) c.Checked)
Dim cbstring As String = cb.Text.ToLower()
f = PredicateExtensions.Or(f, Function(c As Car) c.ColorList.ToLower().Contains(cbstring))
Next
t = If(cbl.Any(Function(c) c.Checked), PredicateExtensions.And(t, f), t)
Return t.ToFunction()
End Function
Private Function GetCheckBoxList(ByVal lv As ListView) As List(Of CheckBox)
Dim result As List(Of CheckBox) = New List(Of CheckBox)
For Each di As ListViewDataItem In lv.Items
For Each c As Control In di.Controls
If (Not TryCast(c, CheckBox) Is Nothing) Then
result.Add(DirectCast(c, CheckBox))
End If
Next
Next
Return result
End Function
A couple of small details necessary for this page:
Private Sub SetFirstPage()
Dim dp As DataPager = lv.FindControl("DataPager")
If Not dp Is Nothing Then
dp.SetPageProperties(0, dp.MaximumRows, False)
End If
End Sub
Protected Sub btnSubmit_Click(sender As Object, e As System.EventArgs) Handles btnSubmit.Click
SetFirstPage()
ViewState("SortExpression") = Nothing
SetArrowImages(Nothing)
End Sub
Protected Sub PagePropertiesChanging(sender As Object, ByVal e As PagePropertiesChangingEventArgs)
_pageclicked = True
End Sub
I'm using a predicate extension above in ColorFilters() and the other checkbox functions, which is just a helper that saved me from importing a full blown predicatebuilder, but I had to translate it from c#.
Public Module PredicateExtensions
<System.Runtime.CompilerServices.Extension()> _
Public Function [And](Of T)(original As Predicate(Of T), newPredicate As Predicate(Of T)) As Predicate(Of T)
Return Function(item) original(item) AndAlso newPredicate(item)
End Function
<System.Runtime.CompilerServices.Extension()> _
Public Function [Or](Of T)(original As Predicate(Of T), newPredicate As Predicate(Of T)) As Predicate(Of T)
Return Function(item) original(item) OrElse newPredicate(item)
End Function
''' <summary>
''' Converts a Predicate into a Func(Of T, Boolean)
''' </summary>
''' <typeparam name="T"></typeparam>
''' <param name="predicate"></param>
''' <returns></returns>
''' <remarks></remarks>
<System.Runtime.CompilerServices.Extension()> _
Public Function ToFunction(Of T)(ByVal predicate As Predicate(Of T)) As Func(Of T, Boolean)
Dim result As Func(Of T, Boolean) = Function(item) predicate.Invoke(item)
Return result
End Function
End Module
And that's it.