Today I had to sort a keyed collection. I have a EntityCollection<T> generic class that inherits from KeyedCollection<PrimaryKey, T> where PrimaryKey is a class that encapsulate a database primary key. At first I checked the .NET class library to see if there’s anything available. It turns out that there’s no sorting capability available for the KeyedCollection<K, T> class unlike the List<T> class which has a Sort() method.
After googling around I found out that the important thing is to use the base class Items collection for the sort by casting it from IList to a List<T> which does support sorting. So I added a Sort method to my EntityCollection<T> like this:
/// <summary>
/// Sort the collection using the specified criteria.
/// </summary>
/// <param name="sortableItems">Items to use for sorting.</param>
public void Sort(params SortingCriteria[] sortableItems)
{
List<T> list = base.Items as List<T>;
if (list != null)
{
ObjectComparer<T> comparer = new ObjectComparer<T>(sortableItems);
list.Sort(comparer);
}
}
After casting the Items collection I create an instance of the ObjectComparer class that inherits from Comparer and is used to compare two items. It looks like this:
class ObjectComparer<K> : Comparer<K>
{
List<SortingCriteria> sortableItems;
public ObjectComparer(params SortingCriteria[] sortableItems)
{
this.sortableItems = new List<SortingCriteria>();
foreach (SortingCriteria item in sortableItems)
{
this.sortableItems.Add(item);
}
}
public override int Compare(K x, K y)
{
if (x == null && y == null)
{
return 0;
}
else if (x == null)
{
return -1;
}
foreach (SortingCriteria item in sortableItems)
{
PropertyInfo propInfo = typeof(K).GetProperty(item.PropertyName);
IComparable xComparer = propInfo.GetValue(x, null) as IComparable;
object yValue = propInfo.GetValue(y, null);
if (xComparer == null)
{
throw new ArgumentOutOfRangeException("Object does not support IComparable interface");
}
int result = xComparer.CompareTo(yValue);
if (result < 0)
{
return (item.Order == SortingOrder.Asceding) ? result : -result;
}
if (result > 0)
{
return (item.Order == SortingOrder.Asceding) ? result : -result;
}
// If equal continue and compare the next property
}
return 0;
}
}
SortingCriteria is a simple class that encapsulate the name of the property and the sorting order as below:
/// <summary>
/// Represent a sorting criteria.
/// </summary>
public class SortingCriteria
{
private string propertyName;
private SortingOrder order;
/// <summary>
/// Create a new sorting criteria, for the specified property name. Sorting order will be ascending.
/// </summary>
/// <param name="propertyName">Name of the property to use for sorting.</param>
public SortingCriteria(string propertyName)
: this(propertyName, SortingOrder.Asceding)
{
}
/// <summary>
/// Create a new sorting criteria, for the specified property name in the specified order.
/// </summary>
/// <param name="propertyName">Name of the property to use for sorting.</param>
/// <param name="order">Sorting order to use.</param>
public SortingCriteria(string propertyName, SortingOrder order)
{
this.propertyName = propertyName;
this.order = order;
}
/// <summary>
/// Gets the name of the property.
/// </summary>
public string PropertyName
{
get
{
return this.propertyName;
}
}
/// <summary>
/// Gets the sorting order.
/// </summary>
public SortingOrder Order
{
get
{
return order;
}
}
}
Finally it uses the Sort() method of the List<T> to do the actual sorting. I added three overloads of the sort method to make it easier to use:
/// <summary>
/// Sort the collection by the specified property, using the specified order.
/// </summary>
/// <param name="propertyName">Name of the property to use for sorting.</param>
/// <param name="order">Sorting order.</param>
public void Sort(string propertyName, SortingOrder order)
{
Sort(new SortingCriteria(propertyName, order));
}
/// <summary>
/// Sort the collection by the specified property, in ascending order.
/// </summary>
/// <param name="propertyName">Name of the property to use for sorting.</param>
public void Sort(string propertyName)
{
Sort(propertyName, SortingOrder.Asceding);
}
/// <summary>
/// Sort the collection using the specified properties in ascending order.
/// </summary>
/// <param name="propertyNames">Name of properties to use for sorting.</param>
public void Sort(params string[] propertyNames)
{
SortingCriteria[] items = new SortingCriteria[propertyNames.Length];
for (int i = 0; i < items.Length; i++)
{
items[i] = new SortingCriteria(propertyNames[i]);
}
Sort(items);
}
This allows the client to use code like:
EntityCollection<Order> orders = Order.GetAll();
orders.Sort(”CustomerName”);
to sort them by the CustomerName property in ascending order, or:
EntityCollection<Order> orders = Order.GetAll();
orders.Sort(”CustomerName”, SortingOrder.Descending);
to sort them in descending order by the CustomerName property, or:
EntityCollection<Order> orders = Order.GetAll();
orders.Sort(”Country”, “CustomerName”);
to sort them by country and then by CustomerName in ascending order.