Sorting and Filtering Custom Collections Part 1
By Tim Price
Requirements
Microsoft® Visual Studio® .NET 2003
Microsoft® Visual Basic .NET
Introduction
This is part one of a two part article; in this part we will create all the necessary class objects to add both filtering and sorting functionality to a custom collection, as well as the custom collection itself. Part 2 will create a .Net windows application that shows how this functionality can be offered to a user of our application. Of course, the application could just as easily be an ASP.net application.
The first thing we will create is a collection of strongly typed objects. We are going to create two objects, one of type Person and a collection object that will act as the container of Person objects, called Persons. This article assumes you are familiar with how and why you would create custom business and collection objects and is therefore, not covered by this article. If, you need more information on this, please read my other articles, Building Strongly Typed Nested Collections.
The Person Object
The person object is made up of three public properties and two overloaded New methods.
Public Class Person
#Region "Private Member Variables"
Private m_sFirstName As String
Private m_sLastName As String
Private m_iAge As Integer
#End Region
#Region "Public Properties"
Public Property FirstName() As String
Get
Return m_sFirstName
End Get
Set(ByVal Value As String)
m_sFirstName = Value
End Set
End Property
Public Property LastName() As String
Get
Return m_sLastName
End Get
Set(ByVal Value As String)
m_sLastName = Value
End Set
End Property
Public Property Age() As Integer
Get
Return m_iAge
End Get
Set(ByVal Value As Integer)
m_iAge = Value
End Set
End Property
#End Region
#Region "Methods"
Public Sub New()
m_sFirstName = String.Empty
m_sLastName = String.Empty
m_iAge = 0
End Sub
Public Sub New(ByVal oNode As XmlNode)
m_sFirstName = oNode.SelectSingleNode("FirstName").InnerText
m_sLastName = oNode.SelectSingleNode("LastName").InnerText
m_iAge = oNode.SelectSingleNode("Age").InnerText
End Sub
#End Region
End Class
The Persons Collection Object
The collection class contains two overloaded New methods, used in object instantiation, a function for adding Person objects to the collection, three functions that return a Persons collection class; used for filtering and a sub routine to sort the collection.
First we will create the overloaded methods and the Add function.
Public Class Persons
Inherits CollectionBase
#Region "Public Methods"
Public Sub New()
End Sub
Public Sub New(ByVal oDOM As XmlDocument)
Try
Dim oNode As XmlNode
For Each oNode In oDOM.DocumentElement.ChildNodes
Dim oPerson As New Person(oNode)
Add(operson)
Next
Catch ex As Exception
Throw New Exception(ex.Message, ex.InnerException)
End Try
End Sub
Public Function Add(ByVal oPerson As Person) As Integer
Return InnerList.Add(oPerson)
End Function
#End Region
There is nothing special or new by the above code and should be nothing new to you.
Providing Filtering
The filtering code is a little more complex, but again, should not cause any headaches to a developer who is familiar with working with custom collections.
First we create two object variables, one of type Person and the other of type Persons. Then we use the person variable to iterate through the contents of the collection class.
For Each oPerson In Me
We then check the respective person public property with the passed in parameter, to see if they are equal. If they are, we just do a quick check to see if the Persons variable has been instantiated and instantiate, if we need to.
If oPerson.FirstName.ToLower = sFirstname.ToLower Then
If oPersons Is Nothing Then
oPersons = New Persons
End If
The person variable we are using to iterate is then added to the newly created Persons collection, by calling it’s Add method.
oPersons.Add(oPerson)
At the end of the iterating, the code needs to know what to return to the caller. It does this by determining whether or not the Persons collection is nothing, if it is, then we know that we did not get a match with any of the existing items in the collection. In this case we return Nothing as the return value, if we did get a match then we simply return the newly created Persons collection.
If oPersons Is Nothing Then
Return Nothing
Else
Return oPersons
End If
Filtering functions need to be created for all the Person object public properties we want to allow the user to filter on. In this case it is the Person.Firstname, Person.Lastname and Person.Age public properties.
#Region "Filtering"
Public Function FilterByFirstname(ByVal sFirstname As String) As Persons
Try
Dim oPerson As Person
Dim oPersons As Persons
For Each oPerson In Me
If oPerson.FirstName.ToLower = sFirstname.ToLower Then
If oPersons Is Nothing Then
oPersons = New Persons
End If
oPersons.Add(oPerson)
End If
Next
If oPersons Is Nothing Then
Return Nothing
Else
Return oPersons
End If
Catch ex As Exception
Throw New Exception(ex.Message, ex.InnerException)
End Try
End Function
Public Function FilterByLastname(ByVal sLastname As String) As Persons
Try
Dim oPerson As Person
Dim oPersons As Persons
For Each oPerson In Me
If oPerson.LastName.ToLower = sLastname.ToLower Then
If oPersons Is Nothing Then
oPersons = New Persons
End If
oPersons.Add(oPerson)
End If
Next
If oPersons Is Nothing Then
Return Nothing
Else
Return oPersons
End If
Catch ex As Exception
Throw New Exception(ex.Message, ex.InnerException)
End Try
End Function
Public Function FilterByAge(ByVal iAge As Integer) As Persons
Try
Dim oPerson As Person
Dim oPersons As Persons
For Each oPerson In Me
If oPerson.Age = iAge Then
If oPersons Is Nothing Then
oPersons = New Persons
End If
oPersons.Add(oPerson)
End If
Next
If oPersons Is Nothing Then
Return Nothing
Else
Return oPersons
End If
Catch ex As Exception
Throw New Exception(ex.Message, ex.InnerException)
End Try
End Function
#End Region
Providing Sorting
The sorting functionality is created by a sub routine called Sort, in this example the sort method accepts two parameters one of type string, which is the field we want to sort and the other is an integer, which represents the direction to sort. For example
- 0 = Ascending
- 1 = Descending
#Region "Sorting"
Public Sub Sort(ByVal sSortItem As String, ByVal iDirection As Integer)
Try
Select Case sSortItem.ToUpper
Case "FIRSTNAME"
Dim oComparer As New Comparer.FirstnameComparer(iDirection)
InnerList.Sort(oComparer)
Case "LASTNAME"
Dim oComparer As New Comparer.LastnameComparer(iDirection)
InnerList.Sort(oComparer)
Case "AGE"
Dim oComparer As New Comparer.AgeComparer(iDirection)
InnerList.Sort(oComparer)
End Select
Catch ex As Exception
Throw New Exception(ex.Message, ex.InnerException)
End Try
End Sub
#End Region
Comparer Class
The real sorting of the collection is handled by a new class object called Comparer which contains three classes that in themselves perform the sorting of the collection. The properties we are going to allow sorting on are the same as the ones used for the filtering:
- Person.Firstname
- Person.Lastname
- Person.Age.
Each of these classes implements the IComparer interface. This interface provides a method for comparing two objects. It allows you to customize the sort order of a collection. This class has a Default function called Compare which implements the actual comparison.
Each of these classes has a New method which accepts a parameter of type integer, this variable represents the direction we want to sort the collection. When the New method is called it sets the value of an internal variable equal to the value of the parameter that was passed in. This variable is then used by the Compare function.
Class Comparer
Class FirstnameComparer
Implements IComparer
Private m_iDirection As Integer
Public Sub New(ByVal iDirection As Integer)
m_iDirection = iDirection
End Sub
Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer Implements System.Collections.IComparer.Compare
Dim oPerson1 As Person
Dim oPerson2 As Person
If GetType(Person).IsInstanceOfType(x) Then
oPerson1 = CType(x, Person)
Else
Throw New Exception("Casting failed...")
End If
If GetType(Person).IsInstanceOfType(y) Then
oPerson2 = CType(y, Person)
Else
Throw New Exception("Casting failed...")
End If
Select Case m_iDirection
Case Enumerations.SortDirection.ASC
Return oPerson1.FirstName < oPerson2.FirstName
Case Enumerations.SortDirection.DESC
Return oPerson1.FirstName > oPerson2.FirstName
End Select
End Function
End Class
End Class
The compare function accepts two parameters of type object and returns an integer corresponding to the sort direction. It starts of by declaring two Person objects. Each of the parameters is tested to see if they are of type person by passing the name of a type (person) to the built in GetType operator and checking the IsInstanceOfType method, which in turn returns a Boolean value.
If GetType(Person).IsInstanceOfType(x) Then
If the parameter is of the type person, we need to convert the object to type person and assign it to one of the variables created at the beginning of the function.
We then determine in which direction the user wants the collection sorting by using a select case statement. This statement uses a Public Enum, defined in a class called Enumerations and illustrated below.
Public Class Enumerations
Public Enum SortDirection
ASC = 0
DESC = 1
End Enum
End Class
Public Enums are out of the scope of this article, but a link is provided in the ‘Further Reading’ section at the end of this article. They simply ‘provide a convenient way to work with sets of related constants and to associate constant values with names’.
We then return to the caller an integer value equal to the direction of the sort. This in turn is passed as a parameter to the InnerList.Sort method, cast your mind back to the definition of the Sort method of the Persons class:
Dim oComparer As New Comparer.FirstnameComparer(iDirection)
InnerList.Sort(oComparer)
The remaining two classes are the same and only differ in the property of the person that we are sorting on and therefore do not need any further discussion.
Class LastnameComparer
Implements IComparer
Private m_iDirection As Integer
Public Sub New(ByVal iDirection As Integer)
m_iDirection = iDirection
End Sub
Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer Implements System.Collections.IComparer.Compare
Dim oPerson1 As Person
Dim oPerson2 As Person
If GetType(Person).IsInstanceOfType(x) Then
oPerson1 = CType(x, Person)
Else
Throw New Exception("Casting failed.")
End If
If GetType(Person).IsInstanceOfType(y) Then
oPerson2 = CType(y, Person)
Else
Throw New Exception("Casting failed.")
End If
Select Case m_iDirection
Case Enumerations.SortDirection.ASC
Return oPerson1.LastName < oPerson2.LastName
Case Enumerations.SortDirection.DESC
Return oPerson1.LastName > oPerson2.LastName
End Select
End Function
End Class
Class AgeComparer
Implements IComparer
Private m_iDirection As Integer
Public Sub New(ByVal iDirection As Integer)
m_iDirection = iDirection
End Sub
Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer Implements System.Collections.IComparer.Compare
Dim oPerson1 As Person
Dim oPerson2 As Person
If GetType(Person).IsInstanceOfType(x) Then
oPerson1 = CType(x, Person)
Else
Throw New Exception("Casting failed.")
End If