Click here to Skip to main content
Licence CPOL
First Posted 22 Nov 2010
Views 12,088
Downloads 71
Bookmarked 27 times

TComparer - Sort a List

By | 14 Dec 2010 | Article
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:

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:

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):

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:

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
public class TComparer<T> : Comparer<T>
{
    private Type _obj;
    private PropertyInfo _property;
    private SortTDirection.Direction _direction;
    ...
}

Let's start with initializing these three variables:

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:

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:

List<T>.Sort()

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

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:

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:

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:

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:

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:

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)

About the Author

Teddy Segoro

Web Developer
TPP Bell Direct
Australia Australia

Member

Follow on Twitter Follow on Twitter
He is a very passionate ASP.NET developer with almost 4 year experience who just found his true joy in programming recently. He is doing both web and software development during work hours and full web development after hours as his freelance work.
 
Used to be a hardcore online gamer - CS, WC3, DoTA, WoW, Team Fortress 2 - but has changed interest a while ago, and now he is truly enjoying web development more than anything else.
 
When not in front of his computer, he usually plays guitar and drum and get involved in various Catholic youth group ministry.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
GeneralMy vote of 5 PinmemberDilip Baboo13:01 9 Mar '11  
Generalnice - have 5 PinmemberPranay Rana22:01 12 Jan '11  
GeneralMy vote of 5 PinmemberAvi Farah1:45 27 Dec '10  
GeneralMy vote of 5 Pinmemberprasad024:10 22 Dec '10  
GeneralMy vote of 5 Pinmembernodar18:01 21 Dec '10  
GeneralMy vote of 4 PinmemberPerry Bruins2:50 15 Dec '10  
GeneralRe: My vote of 4 PinmemberTeddy Segoro17:16 18 Jan '11  
GeneralRe: My vote of 4 PinmemberPerry Bruins21:17 18 Jan '11  
GeneralMy vote of 5 Pinmemberdpflovely20:27 8 Dec '10  
GeneralMy vote of 2 PinmemberMember 15374338:43 29 Nov '10  
GeneralRe: My vote of 2 PinmemberTeddy Segoro15:16 29 Nov '10  
GeneralRe: My vote of 2 PinmemberMember 15374338:53 30 Nov '10  
GeneralRe: My vote of 2 PinmemberTeddy Segoro20:19 2 Dec '10  
General_obj Pinmemberkornman005:57 23 Nov '10  
GeneralRe: _obj PinmemberTeddy Segoro17:52 23 Nov '10  
GeneralRe: _obj PinmemberRichard Deeming6:26 29 Nov '10  
GeneralRe: _obj PinmemberTeddy Segoro15:14 29 Nov '10  
GeneralRe: _obj PinmemberRichard Deeming2:19 30 Nov '10  
GeneralRe: _obj PinmemberTeddy Segoro20:16 2 Dec '10  
Questionjust one question PinmemberMember 364313316:11 22 Nov '10  
GeneralA simple approach Pinmembersenguptaamlan2:08 22 Nov '10  
GeneralRe: A simple approach PinmemberTeddy Segoro17:48 23 Nov '10  
GeneralRe: A simple approach PinmemberServerside4:50 3 Dec '10  
GeneralRe: A simple approach PinmemberTeddy Segoro6:27 3 Dec '10  

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

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.5.120517.1 | Last Updated 15 Dec 2010
Article Copyright 2010 by Teddy Segoro
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid