| Generic Sorting Using Reflection in .NET |
| By Jon Wojtowicz |
|
Download Source Code |
| Microsoft's .Net provides
sorting on ArrayLists and arrays using a general purpose
QuickSort algorithm. To make the sorting work, the Sort methods
use the System.IComparable interface to make the comparisons. This is implemented in the basic types such as string, int,
DateTime, etc. |
| What about my custom
objects? The Sort methods are overloaded and can take a
System.IComparer. By implementing this interface in a class we
can provide a means of sorting our objects. Since the properties
of an object represent available data, we should be able to sort
on any property. |
| Sorting collections of
custom objects or arrays of "stuff" is tedious, boring code that
gets in the way of all the fun tasks in programming. How many
time have you had spend hours writing code similar to Listing
1? This code is also limiting in the manner in which you can
sort. What if my classes were structured as in List 2 and I wanted to sort by the Zip in the Address
property? With the implementation presented, you'd be writing a
lot of IComparers to satisfy all the possible combinations of
sorting a client could ask for. |
| You might just want to sort
coming from the database. This poses it's own set of special
problems such as how do you dynamically create the ORDER BY
clause. Not to mention having to extract the data from the
database every time the user decides to change the sort order. |
| Listing 1. |
public class PersonCollection : CollectionBase
{
public enum SortField
{
FirstName,
LastName
}
...
public void Sort(SortField sortField)
{
IComparer comparer = null;
switch(sortField)
{
case SortField.FirstName:
comparer = new FirstNameComparer();
break;
case SortField.LastName:
comparer = new LastNameComparer();
break;
}
InnerList.Sort(comparer);
}
...
private class FirstNameComparer : IComparer
{
public int Compare(object x, object y)
{
Person p1 = (Person)x;
Person p2 = (Person)y;
return p1.FirstName.CompareTo(p2.FirstName);
}
}
private class LastNameComparer : IComparer
{
public int Compare(object x, object y)
{
Person p1 = (Person)x;
Person p2 = (Person)y;
return p1.LastName.CompareTo(p2.LastName);
}
}
}
|
| Listing 2. |
public class Person
{
private string firstName;
private string lastName;
private StreetAddress address;
public Person(string firstName, string lastName)
{
this.firstName = firstName;
this.lastName = lastName;
}
public string FirstName
{
get{return firstName;}
}
public string LastName
{
get{ return lastName; }
set{ lastName = value; }
}
public StreetAddress Address
{
get{ return address; }
set{ address = value; }
}
}
public class StreetAddress
{
private string street;
public string city;
public string state;
public string zip;
public StreetAddress()
{}
public override string ToString()
{
return street + ", " + city + ", " + state + " " + zip;
}
public StreetAddress(string street, string city, string state, string zip)
{
this.state = state;
this.street = street;
this.city = city;
this.zip = zip;
}
public string Street
{
get{return street;}
set{street = value;}
}
public string City
{
get{return city;}
set{city = value;}
}
public string State
{
get{return state;}
set{state = value;}
}
public string Zip
{
get{return zip;}
set{zip = value;}
}
}
|
| .Net Reflection |
| Reflection is a powerful
feature in .Net. Basically it allows your code to look back at
classes, objects, methods and properties at run time. .Net has
made these features easy to use API in the System.Relection
namespace. There are a lot of good resources for reflection on
the internet and I'll only cover the parts I needed for my
implementation. |
| PropertyComparer |
| The goal of the
PropertyComparer was to eliminate the need to create custom
IComparers for every class and every property. I wanted it to be
flexible enough to be able to sort by any property that
implements IComparable. I also wanted to be able to sort by
properties in contained objects exposed as properties. I decided
to implement this using the doted notation used for referencing
class properties. Using Listing 2, if I wanted to sort the
collection based on the Zip property of the Address property in
the Person class, the sort field would be "Address.Zip". |
| Since I was using
reflection which is a performance hit, I decided to make the
PropertyComparer fixed on a specific property at creation. This
means that I would collect the PropertyDescriptorsCollection
during creation. I walked through the doted notation and
collected all the PropertyDescriptorsCollections for the
contained objects as well. the constructor and supporting method
is in Listing 3. |
| Listing 3. |
public enum CompareOrder
{
Ascending,
Descending
}
public PropertyComparer( string compareField, Type typeToCompare)
{
this.compareField = compareField;
GetPropertyDescriptors(compareField,TypeDescriptor.GetProperties(typeToCompare));
compareOrder = CompareOrder.Ascending;
}
public PropertyComparer( string compareField, Type typeToCompare, CompareOrder compareOrder)
{
this.compareField = compareField;
GetPropertyDescriptors(compareField,TypeDescriptor.GetProperties(typeToCompare));
this.compareOrder = compareOrder;
}
private void GetPropertyDescriptors(string propName, PropertyDescriptorCollection props)
{
propList.Add(propName, props);
int point = propName.IndexOf(".");
if(point > -1)
{
string name = propName.Substring(0, point);
PropertyDescriptor prop = props.Find(name, true);
if( prop == null )
throw new ArgumentOutOfRangeException("Could not find property " + name + ".");
GetPropertyDescriptors(propName.Substring(point + 1),
TypeDescriptor.GetProperties((prop.PropertyType)));
}
}
|
| The Compare method is
fairly small. Most of the work was accomplished in a method
called FindAndCompareProperty. This method can be seen in
listing 4. The code recursively walks the dotted notation
getting the PropertyDescriptorsfor each type specified from the
internal list built in the constructor. It the uses reflection
to obtain the value of the property on each pass. When it gets
to the end of the dotted list it casts the object to IComparable
and calls the compareTo method on the first object. |
| Listing 4. |
private int FindAndCompareProperty(string propName, object x, object y)
{
if (x == null && y == null)
{
return 0;
}
if (x == null)
{
return -1;
}
if (y == null)
{
return 1;
}
PropertyDescriptorCollection props = (PropertyDescriptorCollection)propList[propName];
int point = propName.IndexOf(".");
if(point == -1)
{
PropertyDescriptor prop = props.Find(propName, true);
IComparable compare = prop.GetValue(x) as IComparable;
if(compare == null)
throw new ArgumentException("Property does not implement IComparable.");
return compare.CompareTo(prop.GetValue(y));
}
else
{
string name = propName.Substring(0, point);
PropertyDescriptor prop = props.Find(name, true);
return FindAndCompareProperty(propName.Substring(point + 1),
prop.GetValue(x), prop.GetValue(y));
}
}
|
| Sorting With the PropertyComparer
Class |
| We build an array of Person
objects and sort based on the FirstName property using the code
in Listing 5 . |
| Listing 5. |
string[] firstNames = {"Fred", "Barney", "Wilma", "Betty", "BamBam", "George"};
string[] lastNames = {"Flintstone", "Rubble", "Flintstone", "Rubble", "Rubble", "Slate"};
string[] streets = {"1313 Mockingbird Lane", "503 Fifth St.", "1 Rockingham Lane", "1 Fountain Square",
"1 Dependability Square", "8966 E. 23rd Ave. N."};
string[] cities={"Bedrock", "QuarryStone", "Rock Vegas", "SandStone", "Granite Beach", "Mica Hills"};
string[] states={"FL", "TN", "KY", "VT", "IA", "NV"};
string[] zips = {"61109", "01020", "50811", "41042", "90210", "37420"};
Person[] people = new Person[6];
for(int i = 0; i < 6; ++i)
{
people[i] = new Person(firstNames[i], lastNames[i]);
people[i].Address = new StreetAddress(streets[i], cities[i], states[i], zips[i]);
}
Array.Sort(people, new PropertyComparer("FirstName", typeof(Person), CompareOrder.Ascending));
|
| We can also sort based on
the Zip property of the contained StreetAddress objects. To do
this we would change the Sort() to the following. |
Array.Sort(people, new PropertyComparer("Address.Zip", typeof(Person), CompareOrder.Ascending));
|
| Performance |
| Since this class uses
reflection we expect a performance hit. It is typically 10X
slower than having a plain comparer. The time to sort is
directly proportional to the depth of the sorting on dotted
notation, i.e. sorting on Address.Street takes about twice as
long as sorting on FirstName. the trade-off for the performance
is the simplicity of coding sorting of any array or collection
of custom objects. If you need very high performance then this
approach may not be suitable for you application. |
| Conclusion |
| Reflection is very powerful
but relatively slow. The concept of the PropertyComparer can be
applied to any comparisons, not only sorting. In the code
example I've included a SortableCollectionBase class that allows
performing an IndexOf by specifying the field to match. This can
also be applied to filtering by return a new list of matching
objects. Take a look at the code and imagine the possibilities. |
| Last Update: 6/25/2005 |
|