Click here to Skip to main content
15,885,309 members
Articles / Programming Languages / C#

TComparer - Sort a List

Rate me:
Please Sign up or sign in to vote.
4.65/5 (15 votes)
14 Dec 2010CPOL3 min read 48.1K   167   26   24
Sort a strongly typed list of custom objects by a specific property.

Introduction

If you're still stuck in the ancient world of .NET 2.0 where LINQ and all its conveniences are not born, sorting a strongly typed list can be quite inconvenient. It will require you to:

  • perform a messy delegate(object a, object b){... Comparer<T>.Default.Compare() ...} every time you want to sort, or
  • write a specific comparer for each property of your custom objects

which can be very time-consuming and inefficient.

Using this TComparer class, you can move all those delegate methods into one reusable method, which allows you to sort all your objects by whatever property you want in a single line of code.

Using the code

Before we start, we need System.Collections.Generic to allow us to use List<T> and System.Reflection to provide us with our custom objects' PropertyInfo:

C#
using System;
using System.Collections.Generic;   // provides List<T> and Comparer<T>
using System.Globalization;         // provides method to capitalize first char of a word
using System.Reflection;            // provides PropertyInfo
using System.Runtime.Serialization; // provides SerializationInfo for our custom Exception

To make things more convenient, we create our own enum for sorting order, so we don't rely on a specific ASP control's sort direction enum like System.Web.UI.WebControls.SortDirection etc:

C#
public class SortTDirection
{
    public enum Direction
    {
        Ascending = 0,
        Descending = 1
    }
}

And for better error-handling, we can create our own custom Exception handler (this is optional):

C#
public class PropertyNotFoundException : Exception
{
    public PropertyNotFoundException()
        : base() { }
    public PropertyNotFoundException(string message)
        : base(message) { }
    public PropertyNotFoundException(string message, Exception innerException)
        : base(message, innerException) { }
    public PropertyNotFoundException(SerializationInfo info, StreamingContext context)
        : base(info, context) { }
}

Now this is the interesting bit; TComparer:

C#
public class TComparer<T> : Comparer<T>
{
    ...
}

The reason why we inherit Comparer<T> is because we want to still be able to use Comparer<T>.Default.

Now, to allow our TComparer to perform its sorting, we need to specify three things:

  1. the type of T
  2. the property we want to sort by
  3. the sort direction
C#
public class TComparer<T> : Comparer<T>
{
    private Type _obj;
    private PropertyInfo _property;
    private SortTDirection.Direction _direction;
    ...
}

Let's start with initializing these three variables:

C#
public class TComparer<T> : Comparer<T>
{
    private Type _obj;
    private PropertyInfo _property;
    private SortTDirection.Direction _direction;

    /* new block */
    public TComparer()
    {
        _obj = typeof(T); // we don't care what T is

        // get T's properties
        PropertyInfo[] properties = _obj.GetProperties();
        if (properties.Length > 0)
            _property = properties[0];
        else
            throw new InvalidOperationException(
                String.Format("{0} does not have any property", _obj.Name));

        _direction = SortTDirection.Direction.Ascending;
    }
    /* end of new block */
    ...
}

An assumption we can make: the object to be sorted using TComparer will always have a property; otherwise, you don't have to worry about sorting by property.

You can just specify Comparer<T>.Default as a parameter, or simply leave it blank:

C#
List<T>.Sort(Comparer<T>.Default)
// Or if you want to TComparer to handle the job:
List<T>.Sort(TComparer<T>.Default)
// remember that TComparer inherits Comparer<T>

or:

C#
List<T>.Sort()

Then we need another default constructor which accepts two parameters and sets the property and sort direction:

C#
public class TComparer<T> : Comparer<T>
{
    private Type _obj;
    private PropertyInfo _property;
    private SortTDirection.Direction _direction;

    public TComparer()
    {
        _obj = typeof(T); // we don't care what T is

        // get T's properties
        PropertyInfo[] properties = _obj.GetProperties();
        if (properties.Length > 0)
            _property = properties[0];
        else
            throw new Exception(String.Format(
              "{0} does not have any property", _obj.Name));

        _direction = SortTDirection.Direction.Ascending;
    }
    /* new block */
    public TComparer(string strPropertyName, 
                     SortTDirection.Direction direction)
        : this()
    {
        PropertyInfo p = _obj.GetProperty(strPropertyName);
        if (p != null)
            _property = p;
        else
            throw new PropertyNotFoundException(String.Format(
                "Property {0} does not belong to {1}", 
                strPropertyName, _obj.Name));

        _direction = direction;
    }
    // or if you like to pass a PropertyInfo instead:
    public TComparer(PropertyInfo prop, SortTDirection.Direction direction)
        : this()
    {
        // Set property
        if (prop != null)
            _property = prop;
        else
            throw new PropertyNotFoundException(
                String.Format("The specified PropertyInfo " + 
                              "does not belong to {0}", _obj.Name));
        // Set direction
        _direction = direction;
    }
    /* end of new block */
    ...
}

Here, I prefer having a string as the property parameter (hence, the second default constructor) because it is more convenient to use on the front-end. Of course, passing a string is more insecure, and will require us to perform validation to make sure it is a property of T.

Now, as a mandatory rule of inheriting Comparer<T>, we are required to override the Compare() method. This is where we compare two objects based on which property and in what sort direction we set earlier:

C#
public class TComparer<T> : Comparer<T>
{
    private Type _obj;
    private PropertyInfo _property;
    private SortTDirection.Direction _direction;

    public TComparer()
    {
        _obj = typeof(T); // we don't care what T is

        // get T's properties
        PropertyInfo[] properties = _obj.GetProperties();
        if (properties.Length > 0)
            _property = properties[0];
        else
            throw new Exception(String.Format(
              "{0} does not have any property", _obj.Name));

        _direction = SortTDirection.Direction.Ascending;
    }
    public TComparer(string strPropertyName, 
                     SortTDirection.Direction direction)
    : this()
    {
        PropertyInfo p = _obj.GetProperty(strPropertyName);
        if (p != null)
            _property = p;
        else
            throw new PropertyNotFoundException(String.Format(
                "Property {0} does not belong to {1}", strPropertyName, _obj.Name));

        _direction = direction;
    }
    public TComparer(PropertyInfo prop, SortTDirection.Direction direction)
        : this()
    {
        // Set property
        if (prop != null)
            _property = prop;
        else
            throw new PropertyNotFoundException(
                String.Format("The specified PropertyInfo " + 
                              "does not belong to {0}", _obj.Name));
        // Set direction
        _direction = direction;
    }

    /* new block */
    #region Comparer<T> implementations
    public override int Compare(T a, T b)
    {
        int compareValue = 0;

        // Get the value of property _property in 'a'
        object valA = _property.GetValue(a, null);
        // Get the value of property _property in 'b'
        object valB = _property.GetValue(b, null);

        if (_property.PropertyType.Equals(typeof(DateTime)))
            // DateTime objects must be compared using DateTime.Compare
            compareValue = DateTime.Compare((DateTime)valA, (DateTime)valB);
        else
            // The rest using String.Compare
            compareValue = String.Compare(valA.ToString(), valB.ToString());

        // Reverse order if sort direction is Descending
        if (this._direction == SortTDirection.Direction.Descending)
            compareValue *= -1;

        return compareValue;
    }
    #endregion
    /* end of new block */
    ...
}

Now our TComparer() is ready for use.

To make it more convenient, we can add a static helper method to allow us to use TComparer without having to call new TComparer<T>(...) all the time:

C#
public class TComparer<T> : Comparer<T>
{
    private Type _obj;
    private PropertyInfo _property;
    private SortTDirection.Direction _direction;

    public TComparer()
    {
        _obj = typeof(T); // we don't care what T is

        // get T's properties
        PropertyInfo[] properties = _obj.GetProperties();
        if (properties.Length > 0)
            _property = properties[0];
        else
            throw new Exception(String.Format(
              "{0} does not have any property", _obj.Name));

        _direction = SortTDirection.Direction.Ascending;
    }
    public TComparer(string strPropertyName, 
                     SortTDirection.Direction direction)
        : this()
    {
        PropertyInfo p = _obj.GetProperty(strPropertyName);
        if (p != null)
            _property = p;
        else
            throw new PropertyNotFoundException(String.Format(
                "Property {0} does not belong to {1}", 
                strPropertyName, _obj.Name));

        _direction = direction;
    }
    public TComparer(PropertyInfo prop, 
                     SortTDirection.Direction direction)
        : this()
    {
        // Set property
        if (prop != null)
            _property = prop;
        else
            throw new PropertyNotFoundException(
                String.Format("The specified PropertyInfo " + 
                              "does not belong to {0}", _obj.Name));
        // Set direction
        _direction = direction;
    }

    #region Comparer<T> implementations
    public override int Compare(T a, T b)
    {
        int compareValue = 0;

        // Get the value of property _property in 'a' 
        object valA = _property.GetValue(a, null);
        // Get the value of property _property in 'b'
        object valB = _property.GetValue(b, null);

        if (_property.PropertyType.Equals(typeof(DateTime)))
            // DateTime objects must be compared using DateTime.Compare
            compareValue = DateTime.Compare((DateTime)valA, (DateTime)valB);
        else
            // The rest using String.Compare
            compareValue = String.Compare(valA.ToString(), valB.ToString());

        // Reverse order if sort direction is Descending
        if (this._direction == SortTDirection.Direction.Descending)
            compareValue *= -1;

        return compareValue;
    }
    #endregion
    
    /* new block */
    #region Static helpers
    // pass string as parameter
    public static TComparer<T> SortBy(string strPropertyName, 
                  SortTDirection.Direction direction)
    {
        return new TComparer<T>(strPropertyName, direction);
    }
    // pass PropertyInfo as parameter
    public static TComparer<T> SortBy(PropertyInfo prop, 
                  SortTDirection.Direction direction)
    {
        return new TComparer<T>(prop, direction);
    }
    #endregion
    /* end of new block */
}

Now, let's use our TComparer in an example.

Say we have a sortable object User:

C#
public class User : IComparable
{
    #region Properties
    private int _userID;
    public int UserID
    {
        get { return _userID; }
        set { _userID = value; }
    }

    private string _firstName;
    public string FirstName
    {
        get { return _firstName; }
        set { _firstName = value; }
    }

    private string _lastName;
    public string LastName
    {
        get { return _lastName; }
        set { _lastName = value; }
    }

    private DateTime _DateOfBirth;
    public DateTime DateOfBirth
    {
        get { return _DateOfBirth; }
        set { _DateOfBirth = value; }
    }
    #endregion

    public User()
    {
    }
    public User(object[] values)
        : this()
    {
        UserID = Convert.ToInt32(values[0]);
        FirstName = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(
                    values[1].ToString().Trim());
        LastName = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(
                   values[2].ToString().Trim());
        DateOfBirth = Convert.ToDateTime(values[3]);
    }

    // Default sort
    #region IComparable implementations
    public int CompareTo(object obj)
    {
        return CompareTo((User)obj);
    }
    public int CompareTo(User userObj)
    {
        return TComparer<User>.SortBy("UserID", 
           SortTDirection.Direction.Ascending).Compare(this, userObj);
    }
    #endregion

    ...
}

We use our TComparer to perform the default sort method for User, and here we use "UserID" as the default sort property.

To enable us to test our default constructor which accepts PropertyInfo, we can add a static Find() method to search for a particular property of User:

C#
public class User : IComparable
{
    #region Properties
    public int UserID { get { ; } set { ; } }
    public string FirstName { get { ; } set { ; } }
    public string LastName { get { ; } set { ; } }
    public DateTime DateOfBirth { get { ; } set { ; } }
    #endregion

    public User()
    {
    }
    public User(object[] values)
        : this()
    {
        UserID = Convert.ToInt32(values[0]);
        FirstName = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(
                    values[1].ToString().Trim());
        LastName = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(
                   values[2].ToString().Trim());
        DateOfBirth = Convert.ToDateTime(values[3]);
    }

    // Default sort
    #region IComparable implementations
    public int CompareTo(object obj)
    {
        return CompareTo((User)obj);
    }
    public int CompareTo(User userObj)
    {
        return TComparer<User>.SortBy("UserID", 
          SortTDirection.Direction.Ascending).Compare(this, userObj);
    }
    #endregion

    /* new block */
    #region Static Finder
    public static PropertyInfo Find(string propertyName)
    {
        // We don't care whether the property is found or not,
        // the TComparer<T> default constructor can handle it
        return Array.Find(typeof(User).GetProperties(),
                delegate(PropertyInfo userProp)
                { return userProp.Name == propertyName; })
               as PropertyInfo;
    }
    #endregion
    /* end of new block */
}

Now let's write down a test case:

C#
public static void Main(string[] args)
{
    List<User> users = new List<User>();
    users.Add(new User(new object[] { 4, "DavId", "teNG", new DateTime(1988, 10, 10) }));
    users.Add(new User(new object[] { 0, "teddy", "segoro", new DateTime(1986, 11, 26) }));
    users.Add(new User(new object[] { 3, "NDi", "saPUtrA", new DateTime(1999, 1, 2) }));
    users.Add(new User(new object[] { 2, "leO", "SurYAna", new DateTime(1990, 11, 7) }));
    users.Add(new User(new object[] { 5, "sTevEN", "TjipTo", new DateTime(1977, 9, 8) }));
    users.Add(new User(new object[] { 1, "bEq", "AriF", new DateTime(1984, 9, 3) }));

    // Sort by Default
    Console.WriteLine("Sort by Default:");
    users.Sort(TComparer<User>.Default);
    //users.Sort();
    foreach (User user in users)
    {
        Console.WriteLine(String.Format("{0}, {1}, {2}, {3}", 
                          user.UserID, user.FirstName, user.LastName, 
                          user.DateOfBirth.ToString("dd-MMM-yyyy")));
    }
    Console.WriteLine();

    // Sort by FirstName DESC
    Console.WriteLine("Sort by First Name DESCENDING:");
    PropertyInfo fname = User.Find("FirstName"); // sort by PropertyInfo
    users.Sort(TComparer<User>.SortBy(fname, SortTDirection.Direction.Descending));
    foreach (User user in users)
    {
        Console.WriteLine(String.Format("{0}, {1}, {2}, {3}", 
           user.UserID, user.FirstName, user.LastName, 
           user.DateOfBirth.ToString("dd-MMM-yyyy")));
    }
    Console.WriteLine();

    // Sort by LastName ASC
    Console.WriteLine("Sort by Last Name ASCENDING:");
    PropertyInfo lname = User.Find("LastName"); // sort by PropertyInfo
    users.Sort(TComparer<User>.SortBy(lname, SortTDirection.Direction.Ascending));
    foreach (User user in users)
    {
        Console.WriteLine(String.Format("{0}, {1}, {2}, {3}", 
           user.UserID, user.FirstName, user.LastName, 
           user.DateOfBirth.ToString("dd-MMM-yyyy")));
    }
    Console.WriteLine();

    // Sort by DateOfBirth DESC
    Console.WriteLine("Sort by Date of Birth DESCENDING:");
    //     sort by property name
    users.Sort(TComparer<User>.SortBy("DateOfBirth", 
               SortTDirection.Direction.Descending));
    foreach (User user in users)
    {
        Console.WriteLine(String.Format("{0}, {1}, {2}, {3}", 
           user.UserID, user.FirstName, user.LastName, 
           user.DateOfBirth.ToString("dd-MMM-yyyy")));
    }
    Console.WriteLine();

    Console.ReadLine(); // keep the window open
}

You can see our TComparer is used by passing either string or PropertyInfo :)

And here's the result:

Unable to load image

Version 1.0 (2010-11-22)

  • 1.0: First release.

Version 1.1 (2010-11-23)

  • 1.1: Minor update - Reduced typo and improved unclear descriptions.

Version 2.0 (2010-12-03)

  • Separated SortDirection to another class SortTDirection and renamed into Direction.
  • Added PropertyNotFoundException.
  • Removed redundant code and added better error handling (as suggested by Richard Deeming, *thx Richard*).
  • Added default constructor and static helper that accepts PropertyInfo.
  • Added Find() method in User class to enable us to sort by PropertyInfo instead of string for more precision (as suggested by Member 1537433 *thx..*).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Australia Australia
Teddy is a full-time web/software developer, freelance web developer and a hobbyist indie game developer. He is currently into 2D sprite-based game programming with GameMaker 8.1.

Used to be a hardcore online gamer - Warcraft 3, DoTA, Team Fortress 2, Ragnarok Online, Priston Tale, World of Warcraft, and a few more.

But has found true joy in programming a while ago and has converted since.

When not in front of his computer, he is involved in various Catholic youth ministry, mostly music related.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Dilip Baboo9-Mar-11 13:01
Dilip Baboo9-Mar-11 13:01 
Generalnice - have 5 Pin
Pranay Rana12-Jan-11 22:01
professionalPranay Rana12-Jan-11 22:01 
GeneralMy vote of 5 Pin
Avi Farah27-Dec-10 1:45
Avi Farah27-Dec-10 1:45 
Excellent
GeneralMy vote of 5 Pin
prasad0222-Dec-10 4:10
prasad0222-Dec-10 4:10 
GeneralMy vote of 5 Pin
nodar21-Dec-10 18:01
nodar21-Dec-10 18:01 
GeneralMy vote of 4 Pin
Perry Bruins15-Dec-10 2:50
Perry Bruins15-Dec-10 2:50 
GeneralRe: My vote of 4 Pin
Teddy Segoro18-Jan-11 17:16
Teddy Segoro18-Jan-11 17:16 
GeneralRe: My vote of 4 Pin
Perry Bruins18-Jan-11 21:17
Perry Bruins18-Jan-11 21:17 
GeneralMy vote of 5 Pin
dpflovely8-Dec-10 20:27
dpflovely8-Dec-10 20:27 
GeneralMy vote of 2 Pin
Member 153743329-Nov-10 8:43
Member 153743329-Nov-10 8:43 
GeneralRe: My vote of 2 Pin
Teddy Segoro29-Nov-10 15:16
Teddy Segoro29-Nov-10 15:16 
GeneralRe: My vote of 2 Pin
Member 153743330-Nov-10 8:53
Member 153743330-Nov-10 8:53 
GeneralRe: My vote of 2 Pin
Teddy Segoro2-Dec-10 20:19
Teddy Segoro2-Dec-10 20:19 
General_obj Pin
kornman0023-Nov-10 5:57
kornman0023-Nov-10 5:57 
GeneralRe: _obj Pin
Teddy Segoro23-Nov-10 17:52
Teddy Segoro23-Nov-10 17:52 
GeneralRe: _obj Pin
Richard Deeming29-Nov-10 6:26
mveRichard Deeming29-Nov-10 6:26 
GeneralRe: _obj Pin
Teddy Segoro29-Nov-10 15:14
Teddy Segoro29-Nov-10 15:14 
GeneralRe: _obj Pin
Richard Deeming30-Nov-10 2:19
mveRichard Deeming30-Nov-10 2:19 
GeneralRe: _obj Pin
Teddy Segoro2-Dec-10 20:16
Teddy Segoro2-Dec-10 20:16 
Questionjust one question Pin
Member 364313322-Nov-10 16:11
Member 364313322-Nov-10 16:11 
GeneralA simple approach Pin
senguptaamlan22-Nov-10 2:08
senguptaamlan22-Nov-10 2:08 
GeneralRe: A simple approach Pin
Teddy Segoro23-Nov-10 17:48
Teddy Segoro23-Nov-10 17:48 
GeneralRe: A simple approach Pin
StianSandberg3-Dec-10 4:50
StianSandberg3-Dec-10 4:50 
GeneralRe: A simple approach Pin
Teddy Segoro3-Dec-10 6:27
Teddy Segoro3-Dec-10 6:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.