Behind the Scenes: Improvements to Windows Forms Data Binding in the .NET Framework 2.0, Part 2

 

Cheryl Simmons
Microsoft Corporation

August 2006

Applies to:
   Microsoft Visual Studio 2005
   Microsoft .NET Framework 2.0
   Microsoft Windows Forms

Summary: Learn about the generic BindingList and how to extend this generic collection type to add sorting and searching functionality. (11 printed pages)

Download code samples in C# and Visual Basic (29 KB) at the Microsoft Download Center.

Review Part 1 of this two-part article.

Contents

Introduction
About the Generic BindingList
Overview of the Code Sample
Searching the Generic BindingList
Sorting the Generic BindingList
Next Steps
Conclusion

Introduction

One of the most talked about new features for Windows Forms data binding is the BindingSource component. This component greatly simplifies the developer's job by providing currency management, change notification, and the ability to easily access the members in a bound list. As long as the data source implements the IBindingList interface and has searching and sorting implemented, you can use the BindingSource to search and sort the data source. The DataView class is an example of a type that implements IBindingList. However, sometimes you won't be working with a class that implements IBindingList and will have to create and bind to a list of custom business objects by providing your own implementation of the IBindingList interface. To help with these situations, the Microsoft .NET Framework introduces the generic BindingList type, aka BindingList of T. This article shows how to use the generic BindingList in your data-bound applications to provide searchable, sortable lists of custom business objects.

About the Generic BindingList

The generic BindingList is a generic implementation of the IBindingList interface. The IBindingList interface is an extension of the IList interface, a key interface for Windows Forms data binding. In addition to features inherited from the IList interface, classes that implement IBindingList can optionally support sorting, searching, and change notification. The IBindingList interface is a fairly complex interface and by providing a generic implementation with the generic BindingList type, building bindable lists of custom business objects for consumption by the Windows Forms data binding engine has become much easier. When you use the generic BindingList to store your business objects in a data-binding scenario, the list allows for easy insertion and removal from the list and automatically provides change notification when changes are made to the list. Optionally you can implement searching and sorting on the generic BindingList by overriding a few properties and methods.

Overview of the Code Sample

To demonstrate how to add searching and sorting capability to a data source, I created a simple code sample. Imagine you have a list that contains several Employee objects. The ability to search the list for a particular employee, by name, salary or start date is functionality that you want to offer in your application. In addition to searching the list, users of your application will want to sort the list of Employees by name, salary, hire date, and so on. As you will see in this example, the implementation of sorting on the generic BindingList bound to a DataGridView control allows the DataGridView to offer sorting without additional code.

Introducing the Employee Business Object

The code sample uses a simple Employee business object. The following is the implementation of the Employee type:

public class Employee
{
    private string lastNameValue;
    private string firstNameValue;
    private int salaryValue;
    private DateTime startDateValue;

    public Employee() 
    {
        lastNameValue = "Last Name";
        firstNameValue = "First Name";
        salaryValue = 0;
        startDateValue = DateTime.Today;
    }

    public Employee(string lastName, string firstName, 
        int salary, DateTime startDate)
    {
        LastName = lastName;
        FirstName = firstName;
        Salary = salary;
        StartDate = startDate;
    }

    public string LastName
    {
        get { return lastNameValue; }
        set { lastNameValue = value; }
    }

    public string FirstName
    {
        get { return firstNameValue; }
        set { firstNameValue = value; }
    }

    public int Salary
    {
        get { return salaryValue; }
        set { salaryValue = value; }
    }

    public DateTime StartDate
    {
        get { return startDateValue; }
        set { startDateValue = value; }
    }

    public override string ToString()
    {
        return LastName + ", " + FirstName + "\n" +
        "Salary:" + salaryValue + "\n" +
        "Start date:" + startDateValue;
    }
}

Extending the Generic BindingList

In the code sample, to implement the searching and sorting capability for the list of Employee objects, I created a type named SortableSearchableList based on BindingList. Because the methods and properties you have to override to implement searching and sorting are protected, you must extend the generic BindingList. Also, I chose to make the SortableSearchableList type a generic list, so that you use the <T> syntax in the class declaration. Making the extended list generic lets you reuse the list in multiple situations.

public class SortableSearchableList<T> : BindingList<T>

Searching the Generic BindingList

Implementing search on a generic BindingList requires various steps. First, you have to indicate that searching is supported by overriding the SupportsSearchingCore property. Next, you have to override the FindCore method, which performs the search. Finally, you expose the search functionality.

Indicating Search is Supported

The SupportsSearchingCore property is a protected read-only property that you have to override. The SupportsSearchingCore property indicates whether the list supports searching. By default this property is set to false. You set this property to true to indicate searching is implemented on your list.

protected override bool SupportsSearchingCore
{
    get
    {
        return true;
    }
}

Implementing the Search Funtionality

Next, you have to override the FindCore method and provide an implementation to search the list. The FindCore method searches a list and returns the index of a found item. The FindCore method takes a PropertyDescriptor object that represents the property or column of the data source type to search for and a key object, which represents the value to search for. The following code shows a simple implementation of case-sensitive search functionality.

protected override int FindCore(PropertyDescriptor prop, object key)
{
    // Get the property info for the specified property.
    PropertyInfo propInfo = typeof(T).GetProperty(prop.Name);
    T item;

    if (key != null)
    {
        // Loop through the items to see if the key
        // value matches the property value.
        for (int i = 0; i < Count; ++i)
        {
            item = (T)Items[i];
            if (propInfo.GetValue(item, null).Equals(key))
                return i;
        }
    }
    return -1;
}

Exposing the Search Functionality

Finally, you can expose the search functionality with a public method, or call the search functionality on the underlying IBindingList using its Find method. In order to simplify calls to the search functionality, the publicly exposed Find method shown in the following code takes a takes a string and a key to search for. Next, the code attempts to convert the property string to a PropertyDescriptor object. If the conversion is successful, the PropertyDescriptor and the key are passed to the FindCore method.

public int Find(string property, object key)
{
    // Check the properties for a property with the specified name.
    PropertyDescriptorCollection properties = 
        TypeDescriptor.GetProperties(typeof(T));
    PropertyDescriptor prop = properties.Find(property, true);

    // If there is not a match, return -1 otherwise pass search to
    // FindCore method.
    if (prop == null)
        return -1;
    else
       return FindCore(prop, key);
}

Figure 1 shows the search functionality in action. When you enter a last name and then click the Search button, the search functionality identifies the first row where the last name appears in the grid, if it is found.

Figure 1. Search implemented by using the generic BindingList

Sorting the Generic BindingList

In addition to searching the list, users of your application will want to sort the list of Employees by name, salary, hire date, and so on. You can implement sorting on the generic BindingList by overriding the SupportsSortingCore and IsSortedCore properties and the ApplySortCore and RemoveSortCore methods. Optionally, you can override the SortDirectionCore and SortPropertyCore properties to return the sort direction and property, respectively. Finally, if you want to ensure new items added to the list are sorted, you will have to override the EndNew method.

Indicating Sorting Is Supported

The SupportsSortingCore property is a protected read-only property that you have to override. The SupportsSortingCore property indicates whether the list supports sorting. You set this property to true to indicate sorting is implemented on your list.

protected override bool SupportsSortingCore
{
    get { return true; }
}

Indicating Sorting Has Occurred

The IsSortedCore property is a protected read-only property that you have to override. The IsSortedCore property indicates whether the list has been sorted.

bool isSortedValue;
protected override bool IsSortedCore
{
    get { return isSortedValue; }
}

Applying the Sort

Next, you have to override the ApplySortCore method and provide the actual sorting details. The ApplySortCore method takes a PropertyDescriptor object that identifies the list item property to sort by, and a ListSortDescription enumeration that indicates whether the list should be sorted in ascending or descending order.

In the following code, the list is sorted only if the selected property implements the IComparable interface, which provides a CompareTo method. Most simple types, such as strings, integers, and DateTime types, implement this interface. This is a reasonable decision because all the properties of the Employee object are simple types. In addition, the implementation provides some simple error handling to indicate when a user attempts to sort the data source by a property that is not a simple type.

In the following code, a copy of the unsorted list is saved for use by the RemoveSortCore method. Then the values of the specified property are copied into an ArrayList. A call is made to the ArrayList Sort method, which in turn calls the CompareTo method of the array members. Finally, by using the values in the sorted array, the generic BindingList values are reorganized based on whether the sort is a descending or ascending sort. Finally, a ListChanged event is raised indicating the list has been reset, so bound controls will refresh their values. The following code shows an implementation of the sort functionality.

ListSortDirection sortDirectionValue;
PropertyDescriptor sortPropertyValue;

protected override void ApplySortCore(PropertyDescriptor prop, 
    ListSortDirection direction)
{
    sortedList = new ArrayList();

    // Check to see if the property type we are sorting by implements
    // the IComparable interface.
    Type interfaceType = prop.PropertyType.GetInterface("IComparable");

    if (interfaceType != null)
    {
        // If so, set the SortPropertyValue and SortDirectionValue.
        sortPropertyValue = prop;
        sortDirectionValue = direction;

        unsortedItems = new ArrayList(this.Count);

        // Loop through each item, adding it the the sortedItems ArrayList.
        foreach (Object item in this.Items) {
            sortedList.Add(prop.GetValue(item));
            unsortedItems.Add(item);
        }
        // Call Sort on the ArrayList.
        sortedList.Sort();
        T temp;

        // Check the sort direction and then copy the sorted items
        // back into the list.
        if (direction == ListSortDirection.Descending)
            sortedList.Reverse();

        for (int i = 0; i < this.Count; i++)
        {
            int position = Find(prop.Name, sortedList[i]);
            if (position != i) {
                temp = this[i];
                this[i] = this[position];
                this[position] = temp;
            }
        }

        isSortedValue = true;

        // Raise the ListChanged event so bound controls refresh their
        // values.
        OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
    }
    else
        // If the property type does not implement IComparable, let the user
        // know.
        throw new NotSupportedException("Cannot sort by " + prop.Name +
            ". This" + prop.PropertyType.ToString() + 
            " does not implement IComparable");
}

Removing the Sort

The next step is to override the RemoveSortCore method. The RemoveSortCore method removes the last sort applied to the list and raises a ListChanged event to indicate the list has been reset. The RemoveSort method exposes the remove sort functionality.

protected override void RemoveSortCore()
{
    int position;
    object temp;
    // Ensure the list has been sorted.
    if (unsortedItems != null)
    {
        // Loop through the unsorted items and reorder the
        // list per the unsorted list.
        for (int i = 0; i < unsortedItems.Count; )
        {
            position = this.Find("LastName", 
                unsortedItems[i].GetType().
                GetProperty("LastName").GetValue(unsortedItems[i], null));
            if (position > 0 && position != i)
            {
                temp = this[i];
                this[i] = this[position];
                this[position] = (T)temp;
                i++;
            }
            else if (position == i)
                i++;
            else
                // If an item in the unsorted list no longer exists,
                // delete it.
                unsortedItems.RemoveAt(i);
        }
        isSortedValue = false;
        OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
    }
}

public void RemoveSort()
{
    RemoveSortCore();
}

Exposing Sort Direction and Property

Finally, override the protected SortDirectionCore and SortPropertyCore properties. The SortDirectionCore property indicates the direction of the sort, either ascending or descending. The SortPropertyCore property indicates the property descriptor used for sorting the list.

ListSortDirection sortDirectionValue;
PropertyDescriptor sortPropertyValue;

protected override PropertyDescriptor SortPropertyCore
{
    get { return sortPropertyValue; }
}

protected override ListSortDirection SortDirectionCore
{
    get { return sortDirectionValue; }
}

Exposing the Sort Functionality

In the sample, I bound the custom generic BindingList of Employees (SortableSearchableList) to a DataGridView control. The DataGridView control detects whether sorting is implemented, and because it does, when a column of the DataGridView is clicked, the contents of the DataGridView are sorted by the contents of the column in ascending order. The user can click the column again to sort the items in descending order. The DataGridView makes calls to the list through the IBindingList interface.

Similar to the searching code, you could optionally expose a public method that calls ApplySortCore, or alternatively (like the DataGridView control), make calls to the underlying IBindingList.

((IBindingList)employees).ApplySort(somePropertyDescriptor,
    ListSortDirection.Ascending);

Ensuring Items Added to the List Are Sorted

Since the Employee object exposes a default constructor, the AllowNew property of the BindingList returns true, allowing the user to add additional employees to the list. Since the user can add new items, you also want to ensure that items added to the list are added in sorted order, if a sort has been applied. To do this, I've overridden the EndNew method. In this method override, I check that the list has been sorted by ensuring the SortPropertyValue is not null, I check to see if the new item is being added to the end of the list (indicating the list needs to be resorted), and I then call ApplySortCore on the list.

public override void EndNew(int itemIndex)
{
    // Check to see if the item is added to the end of the list,
    // and if so, re-sort the list.
    if (sortPropertyValue != null && itemIndex == this.Count - 1)
        ApplySortCore(this.sortPropertyValue, this.sortDirectionValue);

    base.EndNew(itemIndex);
}

Figure 2 shows the sort functionality in action. When you click a column in the DataGridView, the contents are sorted. If you click the Remove sort button, the last sort applied is removed.

Figure 2. Sort implemented by using the generic BindingList

Next Steps

As stated earlier, this is a simple example of how you could implement searching and sorting on the generic BindingList. The sorting algorithm that is used in this sample depends on a type implementing the IComparable interface. If you try to sort by a property that does not implement the IComparable interface, an exception will be thrown. This logic works well if you are sorting custom business objects whose properties are mainly simple types. To take the functionality in this sample a step further, you could consider implementing the IComparable interface for any properties of your custom business object that are complex types. For example, if your Employee object has an Address property that is of complex type Address, decide the business rules for sorting the Address type (by street name, number, and so on) and implement the IComparable interface and the CompareTo method on the Address type accordingly.

Additionally, when a new item is added to the end of the list, the list is automatically resorted. Depending on the business logic behind your application, you could consider resorting the list regardless of where an item is added.

Finally, you might want to add filtering and multi-column sorting to your data source. In this case you could consider implementing the IBindingListView interface on your data source.

Conclusion

The BindingSource component is a very useful addition to the Windows Forms data binding story. However, if you are working with lists of custom business objects, you might want to add searching and sorting capabilities. The new generic BindingList makes creating your own searchable and sortable binding lists of custom business objects more straightforward. For more information on these and other features of Windows Forms, see: