smithvoice.com
 Y'herd thisun? 

“Philip also watched and learned from the democratic assembly at Thebes. He saw the grave weakness of a system in which every man could voice his opinion and vote. Debates in the assembly were endless, while political parties worked to destroy the power of their rivals. Philip began to see how an old-fashioned monarchy like Macedonia could act much more decisively than a Greek city and be unstoppable”
-Philip Freeman


from Alexander The Great

Free-form sorting lists of Objects

TaggedCoding, CSharp, VB

Both Arrays and Arraylists implement IList which means that they both expose a Sort method. Thing is that for anything custom you have to define exactly how the elements should compare themselves to other elements for a Sort to happen.

Many resources tell you that sorting elements in these "collections" should be done by implementing IComparable and creating a CompareTo method on the item class itself (if it is not a primative that already has one built-in). For example if you have a Person object then they say that you should create a custom hard-coded CompareTo method in the Person class code that takes the LastName public variable and uses that for the comparisons that add up to a sorted list.

The problem comes when you have an object or struct that could be sorted in a few different ways depending on current needs, even a simple Person object might sometimes be more useful sorted on say an Address property instead of just the last name.

The technique we use is an extension of Chandra Kant Upadhyay's c-sharpcorner example of sorting against a getter (old school getter accessors are most often used these days in facades for simple data binding, to keep values from being automatically displayed). For maximum flexibility we added the option to allow sorts against actual Properties and to tap into a default IComparable.CompareTo if the elements define one.

Drop one of these in your utilities DLL and you can pass it to both an [Array].Sort and [Arraylist].Sort method and both will come out sorted.

VB.Net version:


Public Class GenSorter
Implements IComparer
Public Enum SortOrders As Integer
Ascending = 0
Descending = 1
End Enum
Public Enum AccessorTypes As Integer
Undefined = 0
Method = 1
[Property] = 2
End Enum
Private m_sortAccessorName As String = ""
Private m_sortAccessorType As AccessorTypes = AccessorTypes.Undefined
Private m_sortOrder As SortOrders = SortOrders.Ascending
Public Sub New(ByVal sortAccessorType As AccessorTypes, _
ByVal sortAccessorName As String, ByVal sortOrder As SortOrders)
 
m_sortAccessorName = sortAccessorName
m_sortOrder = sortOrder
m_sortAccessorType = sortAccessorType
End Sub
Public Sub New(ByVal sortOrder As SortOrders)
m_sortOrder = sortOrder
End Sub
Public Function Compare(ByVal x As Object, ByVal y As Object) _
As Integer Implements IComparer.Compare
 
Dim ic1 As IComparable
Dim ic2 As IComparable
Try
If m_sortAccessorType = AccessorTypes.Method Then
 ic1 = CType(x.GetType().GetMethod(m_sortAccessorName).Invoke(x, Nothing), _
IComparable)
 ic2 = CType(y.GetType().GetMethod(m_sortAccessorName).Invoke(y, Nothing), _
IComparable)
ElseIf m_sortAccessorType = AccessorTypes.Property Then
 ic1 = CType(x.GetType().GetProperty(m_sortAccessorName).GetValue(x, Nothing), _
IComparable)
 ic2 = CType(y.GetType().GetProperty(m_sortAccessorName).GetValue(y, Nothing), _
IComparable)
Else
 'undefined in the constructor but the object does have a defined comparer
  
 'c# has the "is" keyword to check an object type information
 'while VB's "Is" continues the tradition of checking the reference
  
 'so in c# you can say "if x is IComparable" while in VB you have to
 'explicitly go after the type info.
 'Here are three ways to do it:
  
 'If Not x.GetType().GetInterface("IComparable") Is Nothing Then ...
 'or:
 'If x.GetType().GetInterface("IComparable") Is GetType(IComparable) 
 'or use TypeOf and Is like this:
 If TypeOf x Is IComparable Then
 ic1 = CType(x, IComparable)
 ic2 = CType(y, IComparable)
 Else
 'no comparer built into the objects, none specified for this instance.
 'That should be an exceptional situation
 Throw New InvalidOperationException("Comparer interface not implemented")
 End If
End If
Catch ex As NullReferenceException
'typically caused by the user mis-spelling or mis-casing the accessor string
'could first do a check of the object's members but an exception would still
'be the result
Throw New NullReferenceException("Specified accessor not found")
End Try
If m_sortOrder = SortOrders.Ascending Then
Return ic1.CompareTo(ic2)
Else
Return ic2.CompareTo(ic1)
End If
End Function
End Class

C# version:

using System;
using System.Collections;
 public class GenSorter : IComparer
{
public enum SortOrders:int
{
 Ascending = 0,
 Descending = 1
}
public enum AccessorTypes:int
{
 Undefined = 0,
 Method = 1,
 Property = 2
}
private String _sortAccessorName = "";
private AccessorTypes _sortAccessorType = AccessorTypes.Undefined;
private SortOrders _sortOrder  = SortOrders.Ascending;
  
public GenSorter(SortOrders sortOrder)
{
 _sortOrder = sortOrder;
}
public GenSorter(AccessorTypes sortAccessorType, string sortAccessorName, _
SortOrders sortOrder)
{
 _sortAccessorName = sortAccessorName;
 _sortOrder = sortOrder;
 _sortAccessorType = sortAccessorType;
}
public int Compare(Object x, Object y)
{
 IComparable ic1 = 0;
 IComparable ic2 = 0;
 try
 {
 if(_sortAccessorType == AccessorTypes.Method)
 {
 ic1 = (IComparable)x.GetType().GetMethod(_sortAccessorName).Invoke(x, null);
 ic2 = (IComparable)y.GetType().GetMethod(_sortAccessorName).Invoke(y, null);
 }
 else if(_sortAccessorType == AccessorTypes.Property)
 {
 ic1 = (IComparable)x.GetType().GetProperty(_sortAccessorName).GetValue(x, null);
 ic2 = (IComparable)y.GetType().GetProperty(_sortAccessorName).GetValue(y, null);
 }
 else
 {
 //note the use of the "is" c# keyword
 //c#'s "is" isn't the same as VB's "Is"
 //VB uses Is to ask about the reference
 //c# uses "is" to ask about the type
 if (x is IComparable)
 {
 ic1 = (IComparable)x;
 ic2 = (IComparable)y;
 }
 else
 {
 //no comparer built into the objects, none specified for this instance.
 //That should be an exceptional situation
 throw new InvalidOperationException("Comparer interface not implemented");
 }
 }
 if(_sortOrder == SortOrders.Ascending)
 {
 return ic1.CompareTo(ic2);
 }
 else
 {
 return ic2.CompareTo(ic1);
 }
  
 }
 
 catch(NullReferenceException)
 {
 //typically caused by the user mis-spelling or mis-casing the accessor string
 //could first do a check of the object's members but an exception would still
 //be the result
 throw new NullReferenceException("specified accessor not found");
 
  
}
}

 

Another option for the C# version's check for the IComparable interface followed by the cast would be to use the c# keyword "as" which does the check then the cast with one call. Thing is that "as" won't throw an exception if the cast is invalid so I think it best to do both steps. By the way, VB8 has a version of "as" and in the tradition of VB's obvious-usage syntax it is called "TryCast".

To use the functions, just create a new instance and pass it to the Array or Arraylist Sort method such as:

Primative (Integer) with arraylist, uses default comparer:


Private Sub butTest_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles butTest.Click
Dim ar As New ArrayList
ar.Add(2)
ar.Add(17)
ar.Add(3)
ar.Add(100)
ar.Add(1)
Dim s As String
For i As Integer = 0 To ar.Count - 1
s &= vbCrLf & CType(ar(i), Integer).ToString
Next
Dim c As New GenSorter(GenSorter.SortOrders.Ascending)
Try
ar.Sort(c)
Catch ex As Exception
MsgBox(ex.ToString)
End Try
s &= vbCrLf & "Sorted: "
For i As Integer = 0 To ar.Count - 1
s &= vbCrLf & CType(ar(i), Integer).tostring
Next
MsgBox(s)
End Sub

Primatives with Array:


Private Sub butTest_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles butTest.Click
Dim ar() As Integer = {2, 17, 3, 100, 1}
Dim s As String
For i As Integer = 0 To ar.Length - 1
s &= vbCrLf & CType(ar(i), Integer).ToString
Next
Dim c As New GenSorter(GenSorter.SortOrders.Ascending)
Try
Array.Sort(ar, c)
Catch ex As Exception
MsgBox(ex.ToString)
End Try
s &= vbCrLf & "Sorted: "
For i As Integer = 0 To ar.Length - 1
s &= vbCrLf & CType(ar(i), Integer).tostring
Next
MsgBox(s)
End Sub

Arraylist sorting a value type structure via the default comparer:


Structure SimplePersonStruct
Implements IComparable
  
Private _LastName As String
  
Public Property LastName() As String
Get
 Return _LastName
End Get
  
Set(ByVal Value As String)
 _LastName = Value
End Set
  
End Property
  
Public Sub New(ByVal value As String)
_LastName = value
  
End Sub
  
Public Function CompareTo(ByVal obj As Object) As Integer _
Implements System.IComparable.CompareTo
  
Return _LastName.CompareTo(CType(obj, SimplePersonStruct)._LastName)
  
End Function
  
End Structure
  
....
  
Private Sub butTest_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles butTest.Click
 
Dim ar As New ArrayList
  
Dim o As New SimplePersonStruct("brown")
ar.Add(o)
  
o = New SimplePersonStruct("zimble")
ar.Add(o)
  
o = New SimplePersonStruct("johnson")
ar.Add(o)
  
Dim s As String
  
For i As Integer = 0 To ar.Count - 1
 s &= vbCrLf & CType(ar(i), SimplePersonStruct).LastName
Next
  
Dim c As New GenSorter(GenSorter.SortOrders.Ascending)
  
Try
 ar.Sort(c)
Catch ex As Exception
 MsgBox(ex.ToString)
End Try
  
s &= vbCrLf & "Sorted: "
For i As Integer = 0 To ar.Count - 1
 s &= vbCrLf & CType(ar(i), SimplePersonStruct).LastName
Next
  
MsgBox(s)
End Sub

Array sort against a Property (uses the same struct defined above):


Private Sub butTest_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) 
Handles butTest.Click
 
Dim ar() As SimplePersonStruct = {New SimplePersonStruct("brown"), _
 New SimplePersonStruct("zimble"), _
 New SimplePersonStruct("johnson)}
 
Dim s As String
 
For i As Integer = 0 To ar.Length - 1
 s &= vbCrLf & CType(ar(i), SimplePersonStruct).LastName
Next
 
Dim c As New GenSorter(GenSorter.AccessorTypes.Property, "LastName", _
GenSorter.SortOrders.Descending)
 
Try
 Array.Sort(ar, c)
Catch ex As Exception
 MsgBox(ex.ToString)
End Try
 
s &= vbCrLf & "Sorted: "
For i As Integer = 0 To ar.Length - 1
 s &= vbCrLf & CType(ar(i), SimplePersonStruct).LastName
Next
 
MsgBox(s)
End Sub

 

We've used a structure for example but of course the exact same functionality will work on objects. Remember that if you use the options to sort off of a property or method name then your string argument has to be spelled and cased correctly.

Hope it helps!

Robert Smith
Kirkland, WA

 



home     who is smith    contact smith     rss feed π
Since 1997 a place for my stuff, and it if helps you too then all the better smithvoice.com