Roll Your Own Sorting in 2012

by MikeHogg 17. November 2012 09:51

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.

Tags:

Linq | VB.Net | ASP.Net

About Mike Hogg

Mike Hogg is a c# developer in Brooklyn.

More Here

Favorite Books

This book had the most influence on my coding style. It drastically changed the way I write code and turned me on to test driven development even if I don't always use it. It made me write clearer, functional-style code using more principles such as DRY, encapsulation, single responsibility, and more. amazon.com

This book opened my eyes to a methodical and systematic approach to upgrading legacy codebases step by step. Incrementally transforming code blocks into testable code before making improvements. amazon.com

More Here