February 2009
Posted on the 3rd at 2:44 PM CST
Sorting a Generic List Dynamically in VB.NET
FiledFiled under VB.NET

In a recent post, I talked about generic lists and how we have been using them at work as a replacement for the DataTable class. For the most part, it has been a real pleasure getting rid of DataTable, but I will admit that there are certain situations in which a DataTable is easier to code for. This is primarily because DataTables are mature and robust, and several helper classes already exist in the .NET framework to process them. The Fill function for the SqlDataAdapter, for example, will automatically load a DataTable with the result of an SQL command for you, which is extremely convenient. There are other classes such as DataView that make manipulative tasks (like sorting and filtering) a piece of cake. For instance, say you have a DataTable populated with about 500 rows and you need to re-sort it. This can be achieved very easily with the DataView class like this…

Private Function SortDataTable(ByRef Table as DataTable, ByVal SortString as String) as DataTable
    Dim View as New DataView(Table)
    View.Sort = SortString
    Return View.ToTable()
End Function


Man, that's almost too easy! It is capable of sorting by multiple fields in any order using a very simple syntax similiar to the "ORDER BY" clause in SQL. As you could probably guess, it is more difficult to sort a generic list. Thanks to the inherent flexibility of generics and reflection, however, it is possible to achieve identical functionality. And I've already done the work to make this happen. The class I created uses reflection, which does not perform fantastically, but it offers up unprecedented flexibility. If performance/efficiency is of noteworthy concern in your application, you might want to consider using a type-specific approach. Also, it was mentioned on Stack Overflow that C# developers can use inline delegates and VB9 developers can use lambda expressions, which I thought was an advanced yet elegant approach. For the sake of simplicity, I just created an instance class that inherits IComparer and will work with any type on the .NET 2.0 framework...

Imports System.Collections
Imports System.Collections.Generic
Imports System.Reflection

Namespace Utility

    ''' <summary>
    ''' Sort order enumeration
    ''' </summary>
    Public Enum SortOrder
        Ascending
        Descending
    End Enum

    ''' <summary>
    ''' This instance class is used to sort a generic collection of object instances.
    ''' It automatically fetches the type and performs the necessary comparison(s) to sort.
    ''' 
    ''' To use, instantiate this class, set the sort string property, and pass this
    ''' instance to the internal Sort() function of your generic collection.
    ''' 
    ''' Example:
    '''     Dim MyList As List(Of MyClassType) = 'Populate the list somehow
    '''     Dim Sorter As New Sorter(Of MyClassType)
''' Sorter.SortString = "Field1 DESC, Field2"
''' MyList.Sort(Sorter) 'After this call, the list is sorted ''' </summary> Public Class Sorter(Of T) Implements IComparer(Of T) Private _Sort As String ''' <summary> ''' Instantiate the class. ''' </summary> Public Sub New() End Sub ''' <summary> ''' Instantiate the class, setting the sort string. ''' ''' Example: "LastName DESC, FirstName" ''' </summary> Public Sub New(ByVal SortString As String) _Sort = SortString End Sub ''' <summary> ''' The sort string used to perform the sort. Can sort on multiple fields. ''' Use the property names of the class and basic SQL Syntax. ''' ''' Example: "LastName DESC, FirstName" ''' </summary> Public Property SortString() As String Get If _Sort IsNot Nothing Then Return _Sort.Trim() End If Return Nothing End Get Set(ByVal value As String) _Sort = value End Set End Property ''' <summary> ''' This is an implementation of IComparer(Of T).Compare ''' Can sort on multiple fields, or just one. ''' </summary> Public Function Compare(ByVal x As T, ByVal y As T) As Integer Implements IComparer(Of T).Compare If Not String.IsNullOrEmpty(Me.SortString) Then Const ERR As String = "The property ""{0}"" does not exist in type ""{1}""" Dim Type As Type = GetType(T) Dim Comp As Comparer = Comparer.DefaultInvariant Dim Info As PropertyInfo For Each Expr As String In Me.SortString.Split(","c) Dim Dir As SortOrder = SortOrder.Ascending Dim Field As String Expr = Expr.Trim() If Expr.EndsWith(" DESC") Then Field = Expr.Replace(" DESC", String.Empty).Trim() Dir = SortOrder.Descending Else Field = Expr.Replace(" ASC", String.Empty).Trim() End If Info = Type.GetProperty(Field) If Info Is Nothing Then Throw New MissingFieldException(String.Format(ERR, Field, Type.ToString())) Else Dim Result As Integer = Comp.Compare(Info.GetValue(x, Nothing), Info.GetValue(y, Nothing)) If Result <> 0 Then If Dir = SortOrder.Descending Then Return Result * -1 Else Return Result End If End If End If Next End If Return 0 End Function End Class End Namespace


The meat of the class lies in the Compare function, which is the essential drivetrain of the IComparer interface. If you are not at all familiar with IComparer, you should check out this article on C# corner about comparing objects; it's quite a useful concept to grasp. The class is fully capable of sorting on one or multiple fields of any type of object. The "sort string" uses the same basic syntax as the ORDER BY clause in SQL, and the field names to use are actually the property names within your class. All you have to do is instantiate the Sorter class, set the type and the sort string, and then pass the class instance to the Sort() function of your generic collection. Following is a simple example (called ConsoleApplication149, for me!) that fully demonstrates the flexibility...

Module Module1

    Sub Main()
        Dim People As List(Of Person) = GetPeople()
        Dim Sorter As New Sorter(Of Person)

        Sorter.SortString = "FirstName"
        People.Sort(Sorter)
        PrintResults(People, Sorter.SortString)

        Sorter.SortString = "LastName DESC, FirstName, MiddleName"
        People.Sort(Sorter)
        PrintResults(People, Sorter.SortString)

        Sorter.SortString = "DateOfBirth DESC"
        People.Sort(Sorter)
        PrintResults(People, Sorter.SortString)

        Console.ReadLine()
    End Sub

    Function GetPeople() As List(Of Person)
        Dim Result As New List(Of Person)

        With Result
            .Add(New Person("John", "M", "Doe", #1/1/1969#))
            .Add(New Person("Jane", "A", "Doe", #3/4/1972#))
            .Add(New Person("Paul", "L", "Smith", #2/1/1948#))
            .Add(New Person("Janet", "A", "Doe", #9/16/1975#))
            .Add(New Person("Patricia", "B", "Smith", #3/14/1952#))
            .Add(New Person("John", "Matthew", "Doe", #12/21/1988#))
            .Add(New Person("James", "L", "Doe", #6/19/1990#))
            .Add(New Person("Patrick", "O", "Smith", #8/26/1993#))
        End With

        Return Result
    End Function

    Sub PrintResults(ByRef People As List(Of Person), ByVal SortString As String)
        Console.WriteLine("Sort String: " & SortString)
        Console.WriteLine()

        For Each Per As Person In People
            Console.WriteLine(Per.ToString())
        Next

        Console.WriteLine()
        Console.WriteLine()
    End Sub
End Module

Class Person

    Private _First As String
    Private _Middle As String
    Private _Last As String
    Private _Dob As Date

    Sub New()

    End Sub

    Sub New(ByVal FirstName As String, ByVal MiddleName As String, ByVal LastName As String, ByVal DateOfBirth As Date)
        _First = FirstName
        _Middle = MiddleName
        _Last = LastName
        _Dob = DateOfBirth
    End Sub

    Property FirstName() As String
        Get
            Return _First
        End Get
        Set(ByVal value As String)
            _First = value
        End Set
    End Property

    Property MiddleName() As String
        Get
            Return _Middle
        End Get
        Set(ByVal value As String)
            _Middle = value
        End Set
    End Property

    Property LastName() As String
        Get
            Return _Last
        End Get
        Set(ByVal value As String)
            _Last = value
        End Set
    End Property

    Property DateOfBirth() As Date
        Get
            Return _Dob
        End Get
        Set(ByVal value As Date)
            _Dob = value
        End Set
    End Property

    Overrides Function ToString() As String
        Return Me.FirstName.PadRight(15, " ") & _
               Me.MiddleName.PadRight(10, " ") & _
               Me.LastName.PadRight(15, " ") & _
               Me.DateOfBirth.ToString("yyyy-MM-dd")
    End Function
End Class


Simple as that! I've grown to love this class, and I hope you too find it useful!

Comments (33)
Permalink Comment from jon labelle on February 4th, 2009 at 7:14 PM
excellent article! keep it up!
Permalink Comment from adam on February 10th, 2009 at 12:55 AM
pls tell me.. why did they throw you out from asp.net forums..!
Permalink Comment from Josh StodolaEmail on February 10th, 2009 at 7:39 AM
I have no idea!
Permalink Comment from adam on February 12th, 2009 at 2:26 AM
anyways... they are the looser (on clientside development!) on this i can say.. :)
Permalink Comment from Craig on February 20th, 2009 at 10:45 AM
Great article -- this is incredibly useful information.
Thanks for sharing!
Permalink Comment from anon on February 24th, 2009 at 3:48 PM
It throws an error on:
Dim Comp As Comparer = Comparer.DefaultInvariant

saying:
Too few type arguments to System.Collections.Generic.Comparer(Of T)
Permalink Comment from Josh StodolaEmail on February 24th, 2009 at 5:07 PM
I don't have that problem. Is it possible that you have namespace confusion? Try to fully qualify it (ie: System.Collections.Comparer).
Permalink Comment from anon on February 25th, 2009 at 7:38 AM
That was it...
System.Collections.Comparer is what it should use.
System.Collections.Generic.Comparer is what is was referencing instead.
DOH <smack>
Permalink Comment from ghadeerEmail on March 18th, 2009 at 10:30 PM
can you help me with my code?
Permalink Comment from GoblinEmail on March 25th, 2009 at 7:28 AM
Thanks a lot for this very useful code.
I had to transform it into c#:

public enum SortOrder
{
Ascending,
Descending
}

public class Sorter<T> : IComparer<T>
{
string _SortString = String.Empty;
public string SortString
{
get { return _SortString.Trim(); }
set { _SortString = value; }
}

public Sorter() { }

public Sorter(string sortstring)
{
_SortString = sortstring;
}

#region IComparer<T> Members

public int Compare(T x, T y)
{
int result = 0;

if (!string.IsNullOrEmpty(SortString))
{
Type t = typeof(T);

Comparer c = Comparer.DefaultInvariant;
System.Reflection.PropertyInfo pi;

foreach (string expr in SortString.Split(new char[] {','}))
{
SortOrder dir = SortOrder.Ascending;
string field;

if (expr.EndsWith(" DESC"))
{
field = expr.Replace(" DESC", String.Empty).Trim();
dir = SortOrder.Descending;
}
else {
field = expr.Replace(" ASC", String.Empty).Trim();
}
pi = t.GetProperty(field);
if (pi != null)
{
result = c.Compare(pi.GetValue(x, null), pi.GetValue(y, null));
if (dir.Equals(SortOrder.Descending))
{
result = -result;
}
if (result != 0)
{
break;
}
}
}
return result;
}
return result;
}
Permalink Comment from gaaeusEmail on March 30th, 2009 at 4:10 AM
Thanks so much for this... Actually saved me a lot of time and effort!
Owe you a beer!
Permalink Comment from An Phu on April 9th, 2009 at 3:00 PM
For what you are doing, it makes sense to use Linq.
You get intellisense and compile-time checking of the the sort fields.

Check out Dynamic methods as a replacement for your Reflection.
Permalink Comment from FlavioEmail on April 27th, 2009 at 7:42 PM
Yeah, I agree with the above comment.
LINQ is perfect in this scenario.

See how beautiful this is:
string[] words = { "aPPLE", "AbAcUs", "bRaNcH", "BlUeBeRrY", "ClOvEr", "cHeRry"};

var sortedWords =
words.OrderBy(a => a.Length)
.ThenBy(a => a, new CaseInsensitiveComparer());
Permalink Comment from Josh StodolaEmail on April 27th, 2009 at 7:46 PM
This post was written for the .NET framework version 2.0, but I appreciate your insight. Thanks.
Permalink Comment from John Williams on May 4th, 2009 at 3:58 PM
Quite useful article and code snippets. Will try such method of sorting in my next app.
Thanks for posting.

John Williams
http://johnwilliams.eu
Permalink Comment from Mauricio BernedoEmail on May 7th, 2009 at 12:17 PM
excellent article!
thanks for sharing!
Permalink Comment from Programmer on May 13th, 2009 at 9:02 AM
Thanks ! Works like charm..
Wonderful effort
Permalink Comment from Jignesh Panchal on May 29th, 2009 at 11:06 AM
Great article. Actually saved a lot of time for me. Thanks for sharing.
Permalink Comment from dotnetnoob on June 23rd, 2009 at 6:35 PM
Great article. Thank for posting.
Permalink Comment from S on July 6th, 2009 at 5:30 AM
Thanks, it's generic enough.
Permalink Comment from Kieran on July 29th, 2009 at 10:43 AM
I was using MVC and for me this worked a lot better, after hours of trapsing all over google looking for something to work.

I have a DB of movies (from the "mvc tutorials"), and have been elaborating on that tutorial to learn mvc.

anyways, this is what I put in my Movie Controller (or home controller, depending on the way you did it):

public ActionResult Movies()
{
List<Movie> moviesTable = _db.MovieSet.ToList();
List<Movie> finalMoviesTable = new List<Movie>();
Movie curMovie = new Movie();
int? highScore = 0;
if (moviesTable.Count != 0)
{
AddNextHighestToList(moviesTable, finalMoviesTable, highScore, curMovie);
}

ViewData.Model = finalMoviesTable;
return View();
}

private void AddNextHighestToList(List<Movie> moviesTable, List<Movie> finalMoviesTable, int? highScore, Movie curMovie)
{
highScore = 0;
foreach (var mov in moviesTable)
{
int? curScore = mov.Score;
if (curScore > highScore)
{
highScore = curScore;
curMovie = mov;
}
}
finalMoviesTable.Add(curMovie);
moviesTable.Remove(curMovie);
if (moviesTable.Count != 0)
{
AddNextHighestToList(moviesTable, finalMoviesTable, highScore, curMovie);
}
else
{
return;
}
}

BASICALLY, moviesTable is the list of movies from the database and finalMoviesTable is what they are going to become, ie the table that will be sorted. The rest is self explanatory so long as you know what everything is.

Hope that helps someone, as it was a REAL PITA to find anywhere to do this (this was the closest solution I found then thought sod it, will come up with one myself).
Permalink Comment from Steve on August 1st, 2009 at 5:09 PM
Brilliant post - exactly what I've spent the last 30minutes looking for!
Permalink Comment from Yusuf on August 13th, 2009 at 4:38 AM
Great article - Many Thanks ;)
Permalink Comment from Boomi on August 13th, 2009 at 1:42 PM
This is exactly what I need. Thanks for sharing.
Permalink Comment from Luis FalconiEmail on August 18th, 2009 at 1:32 PM
it's great thanks.
Permalink Comment from Chris WestEmail on September 11th, 2009 at 9:47 AM
I can't pretend to understand exactly how this works but it does so thanks Josh.

I've made some changes to the list of objects I was sorting and I'm struggling to amend this code to cope with them. Basically I now have a list of objects - Pursuits, and each Pursuit has a PursuitDirector which is an Employee object. The Employee object has a property called Name and it's this that my sort is based on.

Is it possible to sort a list of objects based on this or is it too deeply nested?

Thanks
Permalink Comment from Gagandeep tandon on October 8th, 2009 at 2:49 AM
A class Buddy.... Thanks a LOTTTTTTTTTTTTT
Permalink Comment from Felipe on November 30th, 2009 at 7:31 PM
Thanks so much !!! Great article
Permalink Comment from Lee on December 4th, 2009 at 11:18 AM
Incredibly helpful. Thanks for posting.
Permalink Comment from Web Design Company on January 6th, 2010 at 11:25 PM
Great post.You should continue to post such type of information.
Permalink Comment from David on January 19th, 2010 at 10:35 PM
Great post. By moving as much reflection as possible out of the Compare() method I was able to get it to perform 2-4x times faster:

Imports System.Collections
Imports System.Collections.Generic
Imports System.Reflection

Public Class PropertySorter(Of T)
Implements IComparer(Of T)

Private m_compareType As Type = GetType(T)
Private m_comparer As Comparer = Comparer.DefaultInvariant
Private m_propertyInfo As PropertyInfo
Private m_sortParameters As List(Of KeyValuePair(Of PropertyInfo, SortOrder))

''' <summary>
''' Sort order enumeration
''' </summary>
Public Enum SortOrder
Ascending
Descending
End Enum

Public Sub New(ByVal sortString As String)
SetSortString(sortString)
End Sub

Public Sub SetSortString(ByVal sortString As String)
m_sortParameters = New List(Of KeyValuePair(Of PropertyInfo, SortOrder))

For Each propAndDirection As String In sortString.Split(","c)
Dim direction As SortOrder = SortOrder.Ascending
Dim propName As String

propAndDirection = propAndDirection.Trim()

If propAndDirection.EndsWith(" DESC") Then
propName = propAndDirection.Replace(" DESC", String.Empty).Trim()
direction = SortOrder.Descending
Else
propName = propAndDirection.Replace(" ASC", String.Empty).Trim()
End If

Dim propInfo As PropertyInfo = m_compareType.GetProperty(propName)

If propInfo Is Nothing Then
Throw New MissingFieldException(String.Format("The property ""{0}"" does not exist in type ""{1}""", propName, m_compareType.ToString()))
Else
' create ref to property and direction
m_sortParameters.Add(New KeyValuePair(Of PropertyInfo, SortOrder)(propInfo, direction))
End If
Next
End Sub

Public Function Compare(ByVal x As T, ByVal y As T) As Integer Implements IComparer(Of T).Compare
If m_sortParameters.Count < 1 Then
Return 0
Else
For Each pio As KeyValuePair(Of PropertyInfo, SortOrder) In m_sortParameters

Dim result As Integer = m_comparer.Compare(pio.Key.GetValue(x, Nothing), pio.Key.GetValue(y, Nothing))

If result <> 0 Then
'found difference return result
If pio.Value = SortOrder.Descending Then
result *= -1
End If
Return result
Else
'difference not yet found, keep checking the sort paramet
Permalink Comment from TDog on January 27th, 2010 at 7:04 PM
You rule!!!
Permalink Comment from el on February 22nd, 2010 at 8:33 AM
Recently I have learned about generic list. I have understood little about it.

I can display my data in a datagridview from my database through "list". The problem is I cannot display only one field: "photo" in image type. the error is "unable to cast object of type 'system.byte[]' to type 'system.drawing.image'

My question is how to display my photo in my datagridview. For example in your tuturial in class person, I will add Property photo() As image.

I hope you answer my question.

thanks in advance

Guess What?

There are a few basic guidelines you should be aware of before leaving a comment…

  • If you choose to display your email address, it will not be detected by spam bots
  • Comments are limited to 3,000 characters; so far you have used none of them
  • HTML will be encoded; links and line breaks will be converted automatically
  • Comments containing five or more links will be subject to moderation

Have Your Say

← Answer this to prove you are human
 
 

Chill Out…