The Object Data Source control is one of the best new features of ASP.NET 2.0. The ability to bind collections of Business Objects saves time and produces more elegant, less code-intensive solutions. Unfortunately, if your Business Objects contain child classes binding to Data Controls becomes more difficult. This article demonstrates a technique for sorting a GridView bound to a collection of Objects with a child class. You can download the complete solution.
I created two objects for this example, Employee and Person. The Employee class contains a Person child class. XMLElement properties are defined because the project uses XML Serialization for persistance.
Figure 1: Person Class
public class Person { #region Properties private string _firstName;
[XmlElement("FirstName")] public string FirstName { get { return _firstName; } set { _firstName = value; } } private string _lastName;
[XmlElement("LastName")] public string LastName { get { return _lastName; } set { _lastName = value; } }
#endregion }
Figure 2: Employee Class
[XmlRoot("Employee")] public class Employee { #region Properties private string _jobTitle;
[XmlElement("Title")] public string JobTitle { get { return _jobTitle; } set { _jobTitle = value; } } private int _yearsOfExperience;
[XmlElement("Experience")] public int YearsOfExperience { get { return _yearsOfExperience; } set { _yearsOfExperience = value; } } private Person _person = new Person;
[XmlElement("Person")] public Person Person { get { return _person; } set { _person = value; } }
#endregion }
I would like to display properties from both the Employee and Person class in a GridView control and allow sorting on all columns. For properties in Employee class we can bind using the normal asp:BoundField. For properties in the child class, we need to use a TemplateField and use the Eval method. I added a SortExpression to each column and set AllowSorting to true on the GridView to endable sorting.
Figure 3: The GridView
Sorting & Object BindingNotice the SortParameterName on the ObjectDataSource. The SelectMethod GetEmployees must accept this parameter for sorting to work. The ObjectDataSource passes the SortExpression and ASC or DESC to the SelectMethod when the user clicks a column heading. The GetEmployees method accepts this parameter and returns an array of Employee objects.
Figure 4: GetEmployees Method
public static Employee[] GetEmployees(string xmlPath, string sortExpression) { return GetCollection<Employee>(xmlPath, sortExpression); }
The GetEmployees method in turn calls the Generic GetCollection method. This method can be used to return any collection of objects serialized to an XML docuemnt. The collection is first deserialized, then the Sort Field and Order are parsed, and the collection is sorted with the Sort(comparer) method.
Figure 5: GetCollection<> Method
protected static T[] GetCollection(string xmlPath, string sortExpression) { //deserialize the collection with the xml document XmlSerializer s = new XmlSerializer(typeof(List)); TextReader r = new StreamReader(xmlPath); List list = (List)s.Deserialize(r); r.Close; //if no sort expression was specified, return the unorderd list if (sortExpression == null || sortExpression == string.Empty) return list.ToArray; //split the sort expression w/c is in the form SORTITEM [ASC|DESC] string[] sortOptions = sortExpression.Split(' '); //the field is in the first element of the array string field = sortOptions[0]; //the order is in the second element, if it was specified. Default to ASC string sortOrder = sortOptions.Length > 1? sortOptions[1]: "ASC"; //Sort the list with the Generic Comparer ObjectComparer comparer = new ObjectComparer(field, sortOrder.ToUpper); list.Sort(comparer); return list.ToArray; }
So far, other than using a TemplateField for the child object properties, I haven't done too much out of the ordinary. The ObjectComparer class is what allows us sort the collection. The Compare method is required by any class that implements IComparer. It takes two objects of the same type and returns a zero if they're equal, a negative int if the first is less than the second, and a positive int if the first is greater than the second. Because the field we're comparing may be a property of a child class, like Employee.Person.FirstName, we need to descend the classes to get the value. We do this with the GetPropertyValue method which uses reflection to return the value of a given property. In this example, we first get the value of the Person property, then we get the value of the FirstName property of the Person. After the values of the properties are compared, they are negated if the direction is descending.
Figure 6: ObjectComparer Class
public class ObjectComparer: IComparer { #region Properties private string _direction = "ASC"; public string Direction { get { return _direction; } set { _direction = value; } } private string _compareField; public string CompareField { get { return _compareField; } set { _compareField = value; } }
#endregion
#region Constructors public ObjectComparer(string compareField, string direction) { _compareField = compareField; _direction = direction; } #endregion
#region IComparer Members public int Compare(T x, T y) { //Split the fields into an array. The field is divided by dots, //like Employee.Person.FirstName string[] fieldParts = _compareField.Split('.'); object compareValX = x; object compareValY = y; //get the value of each field in the list. foreach (string field in fieldParts) { compareValX = GetPropertyValue(compareValX, field); compareValY = GetPropertyValue(compareValY, field); } //compare the values of the last fields in the list. Assumes the field's //type implements IComparable. int compareValue = ((IComparable)compareValX).CompareTo(compareValY); //negate the result if dircection is descending if (_direction.ToUpper == "DESC") compareValue = -1 * compareValue; return compareValue; } private object GetPropertyValue(object o, string property) { //using reflection, load the type and return the value of //the given property. This will throw an exception if the //property cannot be found. PropertyInfo pi = o.GetType.GetProperty(property); object val = pi.GetValue(o, null); return val; } #endregion }
That's all we need to do to sort by any field in our class. We could even sort by something like Employee.Person.LastName.Length.
Screenshot: Sortable GridView
