since 1997 a place for my stuff, and if it helps you then so much the better

 
...
...

Free-form sorting lists of Objects


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

added to smithvoice august 2005


...
...

"In theory, theory and practice are the same. In practice, they are not." -Albert Einstein