DataView for objects: Implementation Part IV
Posted on October 22, 2006
Note: You can download the complete implementation here.
In my last post I described the implementation of the IList interface in ObjectListView, my implementation of a view for collections of arbitrary objects. The goal is to provide the same view capabilities for collections that DataView supports for DataTables.
In this post, we’ll move on to the next interface required for data binding, IBindingList.
IBindingList
This interface is the first we’ve discussed that goes beyond the capabilities of ordinary collections. IBindingList seems to be designed for the purpose of interacting with a DataGrid or DataGridView control. In particular, it provides the implementation for the grid column header clicking feature (single column sort), support for the “new” row in the grid (list item construction), and notification of the grid when the collection changes (ListChanged event).
In .NET 1.0 and 1.1, the only implementation of IBindingList was DataView. Hence, DataView has become the defacto reference implementation of the interface, and the model for the behavior that we need to mimic in ObjectListView. In .NET 2.0, we also have BindingList<T> and BindingSource. BindingList<T> is a collection type, which provides no implementation for sorting itself, but rather delegates to a virtual method to be provided in a derived class. BindingSource is a wrapper for a collection, intended to provide data binding support for collections that do not support binding well. Neither of these satisfy our need for a view of a collection, where sorting and filtering does not alter the underlying state of the collection
Sorting
IBindingList adds the SupportsSorting property, which indicates whether other sorting methods of the interface are implemented. This allows a “partial” implementation of IBindingList to still be correct – you could have a non-sorting IBindingList. If SupportsSorting returns true, the following additional methods and properties must be implemented:
- ApplySort()
- RemoveSort()
- IsSorted
- SortDirection
- SortProperty
Of course, these members must be present, even if SupportsSorting returns false (or else your code won’t compile), but it is OK to simply throw a NotImplementedException or perform no operation at all in that case. Callers must always consult SupportsSearching before expecting the other sort members to function correctly.
ObjectListView’s ApplySort looks like this:
public void ApplySort(PropertyDescriptor property, ListSortDirection direction) { Lock(); try { ListSortDescriptionCollection proposed; if (property == null) { proposed = new ListSortDescriptionCollection(); } else { ValidateProperty(property); ListSortDescription[] props = new ListSortDescription[] { new ListSortDescription(property, direction) }; proposed = new ListSortDescriptionCollection(props); } if (IsSortDifferent(proposed)) { this.sortProps = proposed; ApplySortCore(); } } finally { Unlock(); } RaiseListChangedEvents(); }
Here we take a property and direction as parameters. The PropertyDescriptor for a given property of a type can be obtained from one of the GetProperties() methods of TypeDescriptor. For example:
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(typeof(SimpleClass)); this.view.ApplySort(props["SomeProperty"], ListSortDirection.Ascending);
props[“SomeProperty”] obtains the PropertyDescriptor for the property named SomeProperty of type SimpleClass.
The call to ValidateProperty insures that the specified property is a property of the list item type. It also checks to make sure that the type of the property supports IComparable. We’re imposing this constraint so that we will have a known way to compare the property values when we sort.
After we also determine that the proposed sort property differs from the sort already in place (if any), we proceed to the real work in ApplySortCore:
void ApplySortCore() { // Invalidate enumerators. this.version++; if (this.SortIndexesDirty) this.RebuildSortIndexes(); // sorts and filters else this.GetSortIndexes().Sort(CompareItems); this.QueueListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); }
Here we sort our list of indexes, or rebuild them entirely. Recall that the list of indexes is just a list of positions in the underlying list. We use the method CompareItems to compare any two list positions to see which should come first in the index list:
private int CompareItems(int x, int y) { object first = this.list[x]; object second = this.list[y]; if (first == null) { if (second == null) return 0; else return -1; } else { if (second == null) return 1; else { for (int i = 0; i < this.sortProps.Count; i++) { ListSortDescription desc = sortProps[i]; object firstValue = desc.PropertyDescriptor.GetValue(first); object secondValue = desc.PropertyDescriptor.GetValue(second); int result; if (firstValue == null || secondValue == null) { if (firstValue == null) result = (secondValue == null) ? 0 : -1; else result = 1; } else result = ((IComparable)firstValue).CompareTo(secondValue); if (result != 0) { if (desc.SortDirection == ListSortDirection.Descending) result *= -1; return result; } } return 0; } } }
Here we obtain the list items at the two specified positions x and y. Then we get the values of the sort property for each of the two list items, and compare the values using their IComparable implementation. If the sort direction specified in ApplySort was descending, we reverse the return value.
I noted in ApplySortCore() we might rebuild the indexes entirely, instead of sorting them. Why would we do this? Simply because our list of indexes might be inconsistent with the underlying list. Conceptually, this would be because the list isn’t informing ObjectListView of additions to or deletions from the list. Thus, we have an incorrect number of list positions in our index list. We’d prefer the list to always notify us of changes to the list, including changes to the list items themselves. List item changes are important when ObjectListView is sorted and the sort property value of an item changes. At that point the position of the item in the sort may change. How would we be notified of such changes?
ListChanged
The IBindingList interface introduces an event named ListChanged for exactly this purpose. Collections implementing IBindingList must raise ListChanged when a list item is added, deleted, moved, or changed. The ListChanged event is of the delegate type ListChangedEventHandler:
public delegate void ListChangedEventHandler(object sender, ListChangedEventArgs e);
ListChangedEventArgs includes the ListChangedType property, which tells us what was done to the list (add, delete, etc). The NewIndex property specifies the position of the affected item in the list, and OldIndex specifies the original index if the item was moved. It’s interesting to note that when we receive a ListChanged event for ListChangedType.ItemDeleted, the item has already been removed from the list. Also, there is a ListChangedType.Reset, which indicates that a “bulk” change has been made to the list. This is a convenient way of describing an operation like Clear(). Suppose that the list contained 100 items, and Clear was called. Would a caller prefer 100 delete notifications or 1 reset notification? I’d prefer the reset. In any case, our response to a reset must assume that the entire list has changed.
When the underlying list is IBindingList, ObjectListView adds an event handler for the list’s ListChanged event, and can easily keep it’s index list up to date when the list changes. Since ObjectListView itself implements IBindingList, we must also raise our own ListChanged event when we receive a ListChanged event from the underlying list.
As with ApplySort() and SupportsSorting, there is a SupportsChangeNotification property of IBindingList that is paired with the ListChanged event. ListChanged must be raised only if SupportsChangeNotification returns true. Conversely, if SupportsChangeNotification returns false, the DataGridView will not even subscribe to the ListChanged event.
One of the ListChangedTypes provided in a ListChanged event is ListChangedType.ItemChanged. An item change could mean one of two different things. Either an item at the specified list position has been replaced with another item, or a property of the item has a new value. If a property changed, the PropertyDescriptor property of the ListChangedEventArgs will be set to the appropriate value. But how would the underlying list know that a property had changed on one of it’s items?
INotifyPropertyChanged
In .NET 2.0, the INotifyPropertyChanged interface was added to allow an object to indicate that one of it’s property values has changed. This interface consists of only one member, the PropertyChanged event. This event is of the delegate type PropertyChangedEventHandler:
public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);
PropertyChangedEventArgs specifies the name of the property that was changed.
By subscribing to each list item’s PropertyChanged event, the list can be aware of all item changes, and reflect those through it’s own ListChanged event.
Aside: In .NET 1.x, only DataView implemented IBindingList, so when the ListChanged event was raised, an ItemChanged action was understood to mean a change to a value in a DataRow (i.e. an item property change). The concept of replacing one row with another row makes less sense in a DataTable than it does in other collections. The MSDN documentation on ListChanged and ListChangedType.ItemChanged is ambiguous on this point. Additionally, .NET 2.0 adds an interface IRaiseItemChangedEvents, which allows a collection to explicitly state that it raises ListChanged when a property of a list item changes. However, even if IRaiseItemChangedEvents is not implemented, the list may raise ListChanged when an item property changes. DataView does this.
In .NET 1.x (before INotifyPropertyChanged existed), the convention for list items to indicate that one of their property values had changed was to provide a “Changed” event for each property. So, for a property named Color, the presence of an event named ColorChanged would indicate that the object will raise the event when the Color property changes.
Consequences of not providing change notifications
Let’s think about what would happen if a list was bound to a DataGrid, but did not properly support change notifications. If the list does not raise the ListChanged event when items are added or removed from the list, items added to the list will not appear in a DataGrid, and items removed will continue to be displayed. If the list does not reflect item property changes by raising ListChanged, changes to items will not be reflected in the DataGrid. Likewise, if the list items themselves fail to raise PropertyChanged, the list won’t know to raise ListChanged and the DataGrid won’t reflect changes to the list items.
Although I’ve designed ObjectListView to work with any IList type of underlying collection, binding scenarios that require the control to be updated when the list changes will only work if the list supports ListChanged and the items provide property change events. To support .NET 1.x collections, I look for propertyNameChanged style events on the items. I also look for a ListChanged event on the list type, even if the list does not implement IBindingList. If the list does not provide a ListChanged event, ObjectListView will still report list changes to bound controls if the list modification is performed through the methods of ObjectListView, rather than through the list methods directly.
For best results, use a list type that implements (and fully supports) IBindingList, and list item types that implement INotifyPropertyChanged. The easiest list type to use is BindingList<T>, which provides list change notification out of the box, and converts item property changes (INotifyPropertyChanged only) to ListChanged events.
AddNew
An interesting feature of IBindingList is it’s support for the “new row” in the DataGrid. This is the empty row where the user can start typing new data. It’s assumed that a new list item is being manufactured somehow and then added to the list. The mechanism is the AddNew() method of IBindingList. Let’s look at ObjectListView’s implementation:
public object AddNew() { Lock(); ObjectView wrapper = null; try { if (!this.allowNew) throw new DataException("AllowNew is set to false."); object newItem = OnAddingNew(); if (newItem == null) { if (this.itemType == null) throw new InvalidOperationException("The list item type must be set first."); newItem = Activator.CreateInstance(this.itemType); } else { if (this.ItemType == null) this.ItemType = newItem.GetType(); else if (newItem.GetType() != this.ItemType) throw new ArgumentException("Added item type is different from list item type."); } wrapper = new ObjectView(newItem); // If the item type is IEditable object, newly added items don't go into the list until EndEdit is called on the item. // However, ListChanged is still raised. if (this.isEditableObject) { // If an item was previously added with AddNew() but has not yet been committed with item.EndEdit(), commit it now. if (newItemPending != null) FinishAddNew(); this.newItemPending = wrapper; ((IEditableObjectEvents)this.newItemPending).Ended += new EventHandler(editableListItem_Ended); ((IEditableObjectEvents)this.newItemPending).Cancelled += new EventHandler(editableListItem_Cancelled); this.QueueListChanged(new ListChangedEventArgs(ListChangedType.ItemAdded, -1)); } else { int index = this.Add(newItem); } } finally { Unlock(); } RaiseListChangedEvents(); return wrapper; }
There are a few interesting things going on here. First, we call OnAddingNew() to raise the AddingNew event. This gives the ObjectListView user an opportunity to provide a newly created list item. If an item is not provided, we need to create one. Without baking in special knowledge of the concrete list item type, we must resort to creating an instance with the item type’s default constructor.
Once a new list item is created or obtained from the user code, we create an ObjectView wrapper for it. The purpose of this class is to capture editing events if the list item type implements IEditableObject.
The IEditableObject interface indicates that changes can be made to an object, and then committed or cancelled. Typically such an object will retain it’s original data (before IEditableObject.BeginEdit() was called), and a copy of the data that is being edited. When IEditableObject.EndEdit() is called, the edited data takes the place of the original data. If CancelEdit() is called instead, the edited data is discarded.
In the AddNew() method, the addition of the list item is handled in a special way for types that implement IEditableObject. The ListChanged event must be raised when AddNew() is called (with an ItemAdded ListChangedType), and then again when EndEdit() is called on the list item. If CancelEdit() is called on the list item instead of EndEdit(), the addition of the item is cancelled. Between the call to AddNew() and EndEdit()/CancelEdit(), the added item must be kept in a pending state, and not added to the underlying collection.
Following the reference model of DataView, we also commit such a pending added object if AddNew() is called again before EndEdit().
The puzzler here is how ObjectListView would know when EndEdit() or CancelEdit() is called on the list item. IEditableObject does not include any events to indicate that these actions have taken place! My solution is to provide the ObjectView wrapper. ObjectView exposes the newly created list item, and provides it’s own IEditableObject implementation, which delegates to the list item, and also raises events that we can monitor. Here I am using the strategy offered by DataView, which returns a DataRowView from it’s AddNew() implementation, rather than the item type of DataRow. It does this for the same reason, which is to be informed when EndEdit() or CancelEdit() is called on the row.
Other Goop
IBindingList also includes a Find() method which takes a PropertyDescriptor and value as parameters. There are the SupportsXYZ properties, which specify whether the corresponding method of IBindingList is really implemented. The AllowEdit, AllowNew, and AllowRemove properties allow the caller to constrain what actions may be taken through the IBindingList implementation. These are used in conjunction with the bound control; when the DataGrid.AllowNew property is set to false, the new row is hidden, and the AllowNew property of the bound list is set to false.
Finally, there is AddIndex() and RemoveIndex(), which add and remove PropertyDescriptors from indexes used for searching. These methods are not well documented and suggest that the implementations can be a “non-operation”. Not having a clear use case for these methods, I took the advice in the documentation.
Where are we?
I’ve described the IBindingList implementation of a collection view class. This is by far the most important interface for a view to support. It supports change notification, so that bound controls can be kept synchronized with the underlying list. Single property sorting is also provided.
The features still missing are filtering and multi-property sorting. In my next post, I’ll walk through the last interface, IBindingListView, which adds these features.
Got something to say?