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; using System.Globalization; using System.Reflection; using System.Runtime.Serialization;
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:
- the type of
T
- the property we want to sort by
- 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;
public TComparer()
{
_obj = typeof(T);
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;
}
...
}
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)
List<T>.Sort(TComparer<T>.Default)
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);
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()
{
if (prop != null)
_property = prop;
else
throw new PropertyNotFoundException(
String.Format("The specified PropertyInfo " +
"does not belong to {0}", _obj.Name));
_direction = direction;
}
...
}
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);
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()
{
if (prop != null)
_property = prop;
else
throw new PropertyNotFoundException(
String.Format("The specified PropertyInfo " +
"does not belong to {0}", _obj.Name));
_direction = direction;
}
#region Comparer<T> implementations
public override int Compare(T a, T b)
{
int compareValue = 0;
object valA = _property.GetValue(a, null);
object valB = _property.GetValue(b, null);
if (_property.PropertyType.Equals(typeof(DateTime)))
compareValue = DateTime.Compare((DateTime)valA, (DateTime)valB);
else
compareValue = String.Compare(valA.ToString(), valB.ToString());
if (this._direction == SortTDirection.Direction.Descending)
compareValue *= -1;
return compareValue;
}
#endregion
...
}
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);
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()
{
if (prop != null)
_property = prop;
else
throw new PropertyNotFoundException(
String.Format("The specified PropertyInfo " +
"does not belong to {0}", _obj.Name));
_direction = direction;
}
#region Comparer<T> implementations
public override int Compare(T a, T b)
{
int compareValue = 0;
object valA = _property.GetValue(a, null);
object valB = _property.GetValue(b, null);
if (_property.PropertyType.Equals(typeof(DateTime)))
compareValue = DateTime.Compare((DateTime)valA, (DateTime)valB);
else
compareValue = String.Compare(valA.ToString(), valB.ToString());
if (this._direction == SortTDirection.Direction.Descending)
compareValue *= -1;
return compareValue;
}
#endregion
#region Static helpers
public static TComparer<T> SortBy(string strPropertyName,
SortTDirection.Direction direction)
{
return new TComparer<T>(strPropertyName, direction);
}
public static TComparer<T> SortBy(PropertyInfo prop,
SortTDirection.Direction direction)
{
return new TComparer<T>(prop, direction);
}
#endregion
}
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]);
}
#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]);
}
#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
#region Static Finder
public static PropertyInfo Find(string propertyName)
{
return Array.Find(typeof(User).GetProperties(),
delegate(PropertyInfo userProp)
{ return userProp.Name == propertyName; })
as PropertyInfo;
}
#endregion
}
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) }));
Console.WriteLine("Sort by Default:");
users.Sort(TComparer<User>.Default);
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.WriteLine("Sort by First Name DESCENDING:");
PropertyInfo fname = User.Find("FirstName"); 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();
Console.WriteLine("Sort by Last Name ASCENDING:");
PropertyInfo lname = User.Find("LastName"); 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();
Console.WriteLine("Sort by Date of Birth DESCENDING:");
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(); }
You can see our TComparer
is used by passing either string
or PropertyInfo
:)
And here's the result:
Version 1.0 (2010-11-22)
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..*).