ObjectListView Update (1.0.0.3)
Posted on December 3, 2006
Note: You can download the complete implementation here.
Stable Sorts
Pat Dooley asked about making the ObjectListView sorting mechanism stable, an excellent point. This means that list items with sort keys that compare as equal should appear in the same order each time for a given sort. Pat also provided a sample implementation, so I took up the challenge this weekend, and voila: stable sorts.
Since ObjectListView never alters the underlying list while sorting and filtering, we can use the original list order of the items as a reference in resolving the order of equally comparing items. This requires only a small change to the item comparison method:
private int CompareItems(int x, int y) { object first = this.list[x]; object second = this.list[y]; int result; 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); if (firstValue == null || secondValue == null) { if (firstValue == null) { if (secondValue == null) { result = 0; } else result = -1; } else result = 1; } else result = ((IComparable)firstValue).CompareTo(secondValue); // An inequal key was found. if (result != 0) { if (desc.SortDirection == ListSortDirection.Descending) result *= -1; // Record the highest (narrowest) sort property index that // has actually affected the sort. this.lastSortingPropertyIndex = Math.Max(this.lastSortingPropertyIndex, i); return result; } } // All keys are equal; return original item order. result = x > y ? 1 : (x < y ? -1 : 0); // Use the direction of the highest sort property index that has // actually affected the sort to determine comparison direction. // Note that sortProps.Count == 0 when removing the sort. if (sortProps.Count > 0 && sortProps[this.lastSortingPropertyIndex].SortDirection == ListSortDirection.Descending) result *= -1; return result; }
The key is the manipulation of result at the end of the function. If all the keys are equal, we use the original list order (x and y are the list indices) to determine the outcome of the comparison.
A subtlety is determining if we should reverse the list order if the sort direction is descending and the keys compare equally. If there is only one sort property, this is reasonably intuitive; an ascending sort should present the items in list order, and a descending sort should reverse the order. Suppose, however, there are multiple sort keys and only the last sort key compares equally for all of the items. As a user, I wouldn’t expect reversing the sort order on that last column to affect the order of the items. As an example, consider a list of customers with properties Name and Address, where Address is empty for every Customer. If i sort first on Name and second on Address, I’d expect to see the customers in Name order, with matching names in list order. If I reverse the direction of the Address sort, I wouldn’t expect the order to change, since there is no Address data for any of the customers. To meet this expectation, I reverse the comparison result for identical keys only when the sort direction of the last sort key that actually had inequal values is reversed.
BeginUpdate() / EndUpdate
As an IBindingList implementation, ObjectListView is required to raise ListChanged events in a variety of circumstances. If your code responds to these, you’ll find that bulk changes to the underlying list (whether through direct list access or access through the view) cause a lot of events to be raised. This can be a performance problem. To solve this, I’ve added the BeginUpdate() and EndUpdate() pair of methods. The usage is simple: call BeginUpdate() before starting an operation that will perform numerous changes to the list. After BeginUpdate() is called, ListChanged will no longer be raised. Call EndUpdate() when you are done modifying the list. If a ListChanged event would have been raised by your list manipulation (had BeginUpdate() not been called), then EndUpdate() will raise a single ListChanged event with a Reset ListChangedType. An example:
strm = File.OpenRead("customers.dat"); BinaryFormatter fmtr = new BinaryFormatter(); BindingListnewCustomers = (BindingList )fmtr.Deserialize(strm); this.view.BeginUpdate(); this.customers.Clear(); foreach (Customer c in newCustomers) this.customers.Add(c); this.view.EndUpdate();
Here we have an ObjectListView that was constructed over a BindingList of Customers. We clear and reload the customers list from a file inside of a BeginUpdate() / EndUpdate() pair. If a DataGridView were bound to the view, it would be updated only once, when EndUpdate() is called.
ITypedList
The need for ITypedList support occurred to me while writing some demo code. I wanted to bind a DataGridView to an ObjectListView. It’s inconvenient to set the DataSource of the DataGridView to the ObjectListView in the designer. The easy thing to do is to create a new project data source (use the pull-down menu on the DataSource property, and click “Add Project Data Source…”), specifying the list item type. This gives you auto-generated columns that can then be edited as you please. Of course, later you have to set the DataSource to the ObjectListView. If there are no list items in the list at that point, you’ll see an ArgumentException raised with a message to the effect that you cannot bind to the property or column XYZ.
What ITypedList allows the view to do is expose the item properties that can be bound to, even if there are no items in the list. Given an ObjectListView created over a strongly-typed list, or with an ItemType defined, we can easily supply the desired properties.
The ITypedList interface contains two methods, GetListName() and GetItemProperties(). GetListName() is more relevant for a view that exposes a named list, such as a DataView. Since ObjectListView demands only an IList for it’s underlying list, the list may or may not have a name, and we certainly don’t know about it. GetItemProperties() has obvious value for our use case. This method does have a strange argument, though. What is this array of PropertyDescriptors, “listAccessors”? In a nutshell, it’s the path of item properties that leads the code to the properties that should be exposed to the binding component.
You’re probably thinking – Whaaaa? ObjectListView doesn’t have any use for this, but the intent is to expose some properties that are hidden in a hierarchy of properties accessed through the list items. This is important for a control like the DataGrid that can navigate hierarchies of items and their related items (think of a table of customers related to a table of orders). Frans Bouma has a good article that explains the listAccessors used in GetItemProperties(). Here’s the implementation in ObjectListView:
PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors) { if (this.itemProperties == null) throw new InvalidOperationException( "The list item type must be set first."); else return this.itemProperties; } string ITypedList.GetListName(PropertyDescriptor[] listAccessors) { return ""; }
As you can see, we ignore the listAccessors and always return the full set of list item properties. GetListName() returns an empty string.
Enhanced Filter Expressions
I went ahead and added some of the more obvious operators to the Filter property. Now ObjectListView supports:
- =
- !=
- <> (same as !=)
- <
- <=
- >
- >=
Sorted Event
To allow a convenient way of maintaining the current position in a DataGridView bound to an ObjectListView after a sort, I added the Sorted event, which is raised after an explicit call to ApplySort() and also when the view is implicitly resorted by a change to the list or to a list item. What I wanted to do was to move the current position in the DataGridView to the position of the list item that was current before the sort, i.e. the current position should follow the current list item, not stay at the same index position after the sort. With the Sorted event and the PositionChanged event of BindingSource (or CurrencyManager, whichever is convenient), we can track the current list item and update the grid position after the sort.
More to Come
Thomas Jaeger asked about support for generics. This is another good request. We could provide a version of ObjectListView constrained for use with IList<T> instead of the weaker IList. This would afford better performance with no boxing or casting. I may build an ObjectListView<T> in the near future. Also coming soon is an enhanced filtering mechanism, where a custom predicate can be supplied instead of a filter string. I’d also like to add additional richness to the filter string, a la the DataColumn Expression property. As always, your comments and requests are appreciated.
Are you using it?
I’d like to hear about your experience with ObjectListView. What was good? What was bad? What is awkward or complicated? Let me know so that I can make it better!
Got something to say?