Friday, November 21, 2008
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


Print  

pagecomment
  Add Comment



Submit Comment
  View Ratings
50.00%0
40.00%0
30.00%0
20.00%0
10.00%0

Number of Comments 0 , Average of Ratings
  View Comments
No comment.


Privacy Statement  |  Terms Of Use
Copyright 2008 by Robert C. Smith