Jesse Johnston
{ ironic tagline here }

ObjectListView Update (1.0.0.11): Find, Select, & Property Paths

Sunday, 6 May 2007 23:03 by jesse

Note: You can download the complete implementation here.

I just posted a new version of ObjectListView that fixes a couple of bugs and adds some minor usability enhancements.

I've been using ObjectListView more in different projects, and I'm feeling the pain points.  Foremost, a complete IBindingListView implementation means that interaction with Windows Forms controls works smoothly, but that doesn't mean it's easy for the developer to use.

PropertyDescriptors Everywhere

I don't like working with PropertyDescriptors as method parameters.  In order to find a list item via IBindingList,.Find(), you need to get the descriptors for the list item type from TypeDescriptor, and then look up the correct one by the property name.  Awkward.  Moreover, all that Find() gives you is the first item found for which one property equals a specified value.  I've added two more useful Find() overloads.  One takes a string expression that can be an arbitrarily complex set of property comparisons (using the same syntax as the Filter property).  The other allows you to specify a Predicate delegate to provide a comparison in code.  Given these, the original Find() required for IBindingList seems unlikely to be used, so I changed it to an explicit interface implementation.  This has the effect of hiding the overload from the publicly exposed class methods, but still making it available to consumers of IBindingList.

Select

One of the useful methods of DataTable is Select(), which returns the set of DataRows in the table that match a given criteria.  Now clearly, you could iterate over the items in the view yourself and evaluate each, but having this built in to ObjectListView is very convenient.  As with Find(), I added two versions, one that takes a string expression, and the other that takes a delegate.  The return value of Select is a list of items that match the criteria.

Property Paths

Something I've had in mind for a while is being able to filter on list items based on the values of properties of properties.  You could use FilterPredicate to specify such a filter with a delegate, but it would be even more convenient to use an expression like "Customer.AccountRep.Department = 2".  The new version 1.0.0.11 supports this kind of property path in the Filter property, Find() and Select().  Currently, changing the value of one of the sub-properties specified in a Filter property path will not cause the view to be updated.  I need to do more work before I have an efficient implementation of the notification logic.

Item Deletion Events

ObjectListView has always provided the ListChanged event, which specifies an action type (ListChangedType).  One of the actions reported is Deleted, indicating that a list item has been removed.  Unfortunately, only the index of the item deleted is available at the time the event is raised; the item has already been removed.  I've added the RemovingItem event, which is raised just before an item is removed from the list.  This event is only raised when an item is removed through a method of ObjectListView (for example, view.Remove() or view.RemoveAt()).  If an item is removed through a method of the underlying list, the deletion is reported to ObjectListView after the fact, so RemovingItem cannot be raised.

Bugs

As always, a few bugs are fixed, and some minor clean-up done.  See the change log for details.

What's Cooking?

As I mentioned, there's work yet to be done in keeping the view up to date as sub-properties specified in the Filter expression change.  I'm also working on dynamic properties that can be added to the view.  These would be analogous to the expression columns supported by DataTable.

I'm also excited to be presenting ObjectListView at the Portland Code Camp in two weeks!  I think it will be hard to explain it all in an hour.  I'll just have to talk really, really fast.

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

ObjectListView Update (1.0.0.9): Debugger Visualizer

Sunday, 11 March 2007 19:46 by jesse

Note: You can download the complete implementation here.

For those of you just tuning in, ObjectListView is my answer to the .NET need for a DataView-like construct for ordinary objects.  With ObjectListView, you can have a sorted and filtered view of any IList or IList<T>.  You can bind this view to controls like the DataGridView.

This new version adds a debugger visualizer and includes a few other small enhancements and bug fixes.

Bug Fixes

Ecki found a bug in the Master/Details demo where I was doing an invalid cast in the New button click handler.  I also found that .NET 1.1 style property change events weren't wired properly for items already in the list at the time an ObjectListView was constructed.  Those two are fixed.

Boris reported that changing a list item property while iterating over ObjectListView with a foreach loop would cause an InvalidOperationException to be thrown.  Indeed!  This is by design; any change to a .NET collection must invalidate all enumerators of the collection.   ObjectListView is really a view over a collection, though, and not a collection itself.  This view presents the appearance of a collection.  This virtual collection does change when items are added, removed, or replaced in the underlying collection.  When the value of a list item property changes, however, the virtual collection only changes in certain circumstances.  Fortunately, these circumstances are well-understood:  if the property is not included in either the ObjectListView sort or filter, the virtual collection does not change.  In this case, there is no need to invalidate any enumeration in progress.  In the updated version, ObjectListView allows enumeration to continue following a list item property change if the property does not belong to the sort or the filter criteria.

Small Changes

I added a ToArray() method, as I find it irritating to have to declare an array and copy into it with CopyTo() in two steps.  By request, OnListChanged(), OnSorted() and OnAddingNew() are now protected virtual, for extensibility.  These methods follow the usual convention of event-raising methods in .NET.  If you override one of them, be sure to call the base version so that the event will in fact be raised.

License

Since more than a few have asked, I've added the license terms to the Readme.txt file of the download.  ObjectListView is free - no strings attached.

The Visualizer

This is the big cool addition in my estimation.  If you're debugging a program that uses ObjectListView, you can see a nice dialog that shows you the current state of the ObjectListView and it's underlying list.  At a breakpoint, move the mouse over the variable that is the ObjectListView, and click the little magnifying glass.  You'll see something like this:

On the left side, you'll see information about the type of objects in the underlying list ("List Item Type"), and below that, information about the list itself ("List Type").  If the ObjectListView is sorted, the sort properties and sort direction of each are listed.  Below that is the current filter expression, if any.  If you're using a filter predicate (as I am in the example), it will tell you that.

The right side shows two tabs.  The one on the top ("View") shows the list items that are currently exposed by the ObjectListView, in the order defined by the current sort, and excluding any items that are filtered out.  This is exactly what you would see in a foreach loop, enumerating the items in the ObjectListView.  The second tab ("List") shows all of the list items in the underlying list, in the order in which they appear in the list.  Cool, huh?

But wait, there's more!  If you're having problems getting ObjectListView to work the way that you expect it to, check out the Analysis button in the lower left.  If you click that, you'll see a synopsis of how the list and list item types work with ObjectListView.  It will tell you about any potential problems, and recommend a solution.  Here's what it looks like:

I hope that the visualizer is helpful.  Debug visualizers are really not very hard to write, and I encourage you to write your own for any complex types that you're coding up.  Take a look at the source in ObjectListViewVisualizer.cs to see how to do it.  The only significant constraint on your code is that the type you're visualizing needs to be serializable.

Enjoy - and please don't hesitate to speak up if you have questions or thoughts about ObjectListView!

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

ObjectListView Update (1.0.0.8): Complex Filter Expressions

Monday, 26 February 2007 23:41 by jesse

Note: You can download the complete implementation here.

A few people have asked for support of more sophisticated expressions in the Filter property.  Previously, ObjectListView allowed only a single relational expression of the form propertyName Op value, such as Name = 'Smith'.  With this update, you can assign arbitrarily complex expressions to Filter.  For example: Name = 'Smith' AND (Orders > 50 OR City = 'Portland').

Details

The format of the Filter expression is similar to that of the comparison expressions that can be assigned to DataColumn.Expression.  Each term is a comparison of the form propertyName relationalOp value, and terms can be connected with the Boolean operators AND or OR.

The relational operators that may be used include:

=

== (synonymous with =)

!=

<> (synonymous with !=)

<

<=

>

>=

The value to the right of a relational operator must be a literal; it cannot be the name of another property (a property name will just be interpreted as literal text).  String values must be quoted with single or double quotes if they contain spaces or relational operators.  If a string value contains a single or double quote, quote the value with the other quote character (e.g. "O'Reilly").

The Boolean operators AND and OR are case-insensitive.  NOT is not currently supported.  The AND operator takes precedence over the OR operator.  For example, Name = 'Smith' AND Orders > 50 OR City = 'Portland' requires either both the Name and the Orders comparison to succeed, or the City comparison to succeed.

Parentheses can be used to alter the order of precedence, and can be nested.

Changes to any of the referenced property values in any list item will cause re-evaluation of the filter.  In the above example, changes to the Name, Orders, and City property values will cause re-evaluation.

ApplyFilter()

This new method has been added to better support the FilterPredicate property.  FilterPredicate is used when the filter criteria cannot be easily expressed with the Filter property.  To use it, you set the FilterPredicate to a delegate referencing your own function.

Some typical use cases for FilterPredicate include "sub-property" filter criteria, and dynamic criteria.  The sub-property case is one in which a list item property is a class that itself has properties, and you want to filter on one of the properties of the property.  An example of dynamic criteria is a combobox selection that determines the filter criteria.

The problem is that previously, ObjectListView didn't know when to re-apply the FilterPredicate.  The filter criteria would be applied when the FilterPredicate was set, and when any list item property changed.  Both of the above use cases require the criteria to be re-evaluated at times when the list item properties aren't changing.  In neither case does (or can) ObjectListView have enough information to know to update the filter.

The solution is to expose ApplyFilter() to allow the developer to re-apply the filter when needed.  Note that currently, any list item property change will cause the filter to be re-applied, so if you're just filtering on a list item property in your callback method, you don't need to call ApplyFilter().  Also, you don't need to call ApplyFilter() when you're using the Filter property; it is only needed when using FilterPredicate.

Gory Details

The complex filter expression required a "real" parser and expression-tree evaluator, unlike the cheesy but simple parsing I was able to get away with for a single relational expression.  Most of this is traditional computer science stuff and probably doesn't deserve much discussion.  The nifty part of it is the how the expression evaluation is exposed to ObjectListView as a simple predicate delegate, in the same way that a user-supplied FilterPredicate is used.  If you're interested, see the FilterNode and FilterEvaluator classes.  Let me know if you see any obvious improvements - this isn't the kind of work I do on a daily basis, so I have no doubt that it could be better.

What's up next?

I promised Peter an ObjectListView visualizer for Visual Studio, so that's next on my agenda.  Mihail commented about exposing list item properties in a specific order, which led me to some ideas about providing extended list item properties through ObjectListView.  This might be interesting when putting a view over some objects that you don't have source for (or can't change for some other reason), and you need expression-style derived properties or something more.  Anyway, I'll report back as I work through the details.

Until next time, happy viewing!

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Generics in ObjectListView

Tuesday, 2 January 2007 00:07 by jesse

Note: You can download the complete implementation here.

My original implementation of ObjectListView wraps a list of arbitrary objects.  It exposes these list items as type "object", which means that you have to cast the returned items to your list item type.  For example:

List<SimpleClass> list = new List<SimpleClass>();
ObjectListView view = new ObjectListView(list);
 
list.Add(new SimpleClass(1, "aaa", DateTime.Now));
list.Add(new SimpleClass(5, "bbb", DateTime.Now));
 
view.FilterPredicate = delegate(object listItem) { return ((SimpleClass)listItem).IntegerValue == 5; };
 
Assert.AreEqual("bbb", ((SimpleClass)view[0]).StringValue);

Here we have to cast the result of view[0] back to SimpleClass.

Wouldn't it be easier to use an ObjectListView that exposed the list items as their actual type?

ObjectListView<T>

You asked for it, and here it is.  In version 1.0.0.7, I've added a version of ObjectListView that fully supports generics.  The basic idea is that ObjectListView<T> takes an IList<T> as it's constructor argument.  The original ObjectListView takes a regular IList argument.  Thus, we can re-write the above example this way:

List<SimpleClass> list = new List<SimpleClass>();
ObjectListView<SimpleClass> view = new ObjectListView<SimpleClass>(list);
 
list.Add(new SimpleClass(1, "aaa", DateTime.Now));
list.Add(new SimpleClass(5, "bbb", DateTime.Now));
 
view.FilterPredicate = delegate(SimpleClass listItem) { return listItem.IntegerValue == 5; };
 
Assert.AreEqual("bbb", view[0].StringValue);

Because List<T> implements both IList and IList<T>, we can use the same underlying List<SimpleClass> for ObjectListView and ObjectListView<SimpleClass>.

Note that in addition to the indexer returning SimpleClass (e.g. view[0]), the FilterPredicate property is now a delegate type that takes an instance of the list item type (T) as a parameter, instead of type object.

Differences between ObjectListView and ObjectListView<T>

With ObjectListView<T>, the list item type is expressed right in the declaration of the view: ObjectListView<SimpleClass> is a view of SimpleClass list items.  This means that the ItemType property is not needed in ObjectListView<T>.  It also obviates the need for internal checks to see that the list item type is specified and that the list items are homogeneous.

Interestingly, IList<T> does not derive from IList.  This has important ramifications for ObjectListView.  It's primary role is to implement IBindingListView, which derives from IBindingList, IList, ICollection, and IEnumerable.  So we're not off the hook for supporting those interfaces.  Assuming that the consumer of ObjectListView<T> will be most interested in using the generic interfaces, I've chosen to make the weakly-typed interfaces explicit implementations.  This means that both IList and IList<T> are supported, but the methods of IList<T> are the public ones.  If you want to access an IList method, you just need to cast ObjectListView<T> to an IList first.

ICollection<T> (the base of IList<T>) doesn't offer the IsSynchronized or SyncRoot members of ICollection.  The consequence of this is that ObjectListView<T> need not worry about thread synchronization with the underlying list; there isn't anything to synchronize with.  Similarly, IList<T> does not provide the IsFixedSize property of IList, which eliminates more housekeeping code.

Both the event arguments of the AddingNew event and the ObjectView wrapper returned by AddNew() are strongly typed now.

As noted in the code example above, the ListItemFilter delegate type used by the FilterPredicate property is also strongly typed.

Updated Demo

I changed the Master/Details demo included in the download to use the generic version of ObjectListView.  I also added menu options to demonstrate filtering the list displayed in the grid.

Of course, the original "non-generic" version of ObjectListView is still included in the download, so if you prefer that, by all means use it!

Happy New Year!

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

ObjectListView Update (1.0.0.6)

Wednesday, 27 December 2006 19:00 by jesse

Note: You can download the complete implementation here.

The DataGridView New Row

When you set the DataGridView property AllowUserToAddRows to true (and the IBindingList data source AllowNew property is also true), an empty row will be displayed at the bottom of the grid.  In an earlier posting, I described how the IBindingList method AddNew() supports this "new row".

When I implemented AddNew in ObjectListView, I noted this comment in the MSDN documentation of the method:

If the objects in this list implement the IEditableObject interface, calling the CancelEdit method should discard an object, not add it to the list, when the object was created using the AddNew method. The object should only be added to the list when the IEditableObject.EndEdit method is called. Therefore, you must synchronize the object and the list carefully.

I interpreted this to mean that list items added via AddNew() (at least IEditableObject items) should not actually be added to the underlying list until EndEdit() is called on the list item.

Unfortunately, this doesn't work correctly.  Once AddNew() has been called, the DataGridView expects to be able to access the newly added row.  The item needs to be available through the view class, but also needs to be kept in a provisional state so that it can be removed should the user later cancel the add with a call to IEditableObject.CancelEdit().  DataView handles this nicely by creating a new DataRow with a call to DataTable.NewRow(), which leaves the row in a Detached state, and not in the DataTable.  Once the row is committed, the DataRow is added to the DataTable.  DataView also is careful to make the row available through it's indexer, so that the new DataRow appears to have been added to the collection.

I also observed that although ObjectListView.AddNew() returns an IEditableObject wrapper to allow calls to IEditableObject.EndEdit() and CancelEdit() to be captured, DataGridView ignores the returned object and accesses the underlying wrapped object through the ObjectListView indexer.  This means that newly added list items are never committed.  This also got me wondering how I could support a cancelable AddNew() operation for list items that don't implement IEditableObject.

ICancelAddNew

When DataGridView commits a row that was added with AddNew(), it first checks to see if the list item implements IEditableObject.  If so, IEditableObject.EndEdit() is called.  Next, the data source is checked to see if it implements ICancelAddNew.  If so, ICancelAddNew.EndNew() is called.  Similarly, canceling the previous AddNew() is done by calling IEditableObject.CancelEdit() and ICancelAddNew.CancelNew().  Since the methods of ICancelAddNew are expected to be provided by the data source, we can implement them very directly.  Contrast this to committing/canceling through the list item methods of IEditableObject, which requires wrapping the list item in an IEditableObject implementation that also raises an event or has some special knowledge of the view class.  DataView supplies a DataRowView wrapper that has a reference to the DataView, and can thus inform the DataView when EndEdit() or CancelEdit() is called.  Key to the DataView/DataRowView implementation is the fact that DataView exposes the DataRowView wrapper as the list item type in all cases.  AddNew() returns a DataRowView, as does the indexer and the enumerator.

A goal of ObjectListView is to support as transparent a view as possible over the user-supplied collection.  This means that the methods referring to list items always return or take as a parameter the actual list item, not a wrapper.  This led me to reject the DataRowView-style wrapper approach of DataView, with the exception of AddNew(), which must support commit/cancel through IEditableObject.

By adding support for ICancelAddNew, we gain the ability to cancel list additions with no penalty to the usage model.  The implementation is straightforward:

void ICancelAddNew.CancelNew(int itemIndex)

{

    Lock();

 

    try

    {

        if (this.IsPendingNewItem(itemIndex))

            CancelAddNew();

    }

    finally

    {

        Unlock();

    }

 

    RaiseEvents();

}

void ICancelAddNew.EndNew(int itemIndex)

{

    Lock();

 

    try

    {

        if (this.IsPendingNewItem(itemIndex))

            FinishAddNew();

    }

    finally

    {

        Unlock();

    }

 

    RaiseEvents();

}

In AddNew(), we save a reference to the newly added item so that we can refer to it here.  As you might note, CancelNew() and EndNew() operate on a particular list position.  If the item at that position is not the previously added item, the call is ignored.

The actual commit and cancel methods are shared for both ICancelAddNew and IEditableObject approaches.  This allows us to use IEditableObject list items and commit them programmatically, or use the new row of DataGridView and commit implicitly via the support for ICancelAddNew:

private void FinishAddNew()
{
    if (this.isEditableObject)
    {
        ((IEditableObjectEvents)this.newItemPending).Ended -= new EventHandler(editableListItem_Ended);
        ((IEditableObjectEvents)this.newItemPending).Cancelled -= new EventHandler(editableListItem_Cancelled);
    }
 
    // Raise the second ListItemChanged / ItemAdded event.
    if (this.isEditableObject)
    {
        int index = this.IndexOf(this.newItemPending.Object);
        this.QueueEvent(new ListChangedEventArgs(ListChangedType.ItemAdded, index));
    }
 
    this.newItemPending = null;
 
    // Now that the item is committed, reposition it in the sort, and potentially filter it out of the list.
    if (this.IsFiltered)
        ApplyFilter();
    if (this.IsSorted)
        ApplySortCore();
}
private void CancelAddNew()
{
    if (this.isEditableObject)
    {
        ((IEditableObjectEvents)this.newItemPending).Ended -= new EventHandler(editableListItem_Ended);
        ((IEditableObjectEvents)this.newItemPending).Cancelled -= new EventHandler(editableListItem_Cancelled);
    }
 
    ObjectView temp = this.newItemPending;
 
    this.newItemPending = null;
 
    this.Remove(temp.Object);
}
private bool IsPendingNewItem(int listIndex)
{
    return (this.newItemPending != null && list.IndexOf(this.newItemPending.Object) == listIndex);
}

This is reasonably self-explanatory.  If the list item type implements IEditableObject, the added item is a wrapper class instance that provides Ended and Cancelled events.  AddNew() subscribed to these events, and they are unwired here.  For IEditableObject list items, the MSDN documentation (and the observed behavior of our DataView reference model) requires us to raise the ListChanged (ItemAdded) event a second time during the commit.

Other Special Aspects of the New Row

The list item represented by the "new row" has some other subtleties.  Consider what happens when the grid is sorted.  If you're entering data in the new row, you wouldn't expect that row to move somewhere else in the grid while you are typing.  However, if the list item has been added to the view, it will be repositioned when the sort demands it.  To prevent this, I changed the item add logic to put the new item at the end of the view collection, and simply append that position to the sort indexes:

private int OnItemAdded(int listIndex)
{
    // Invalidate enumerators.
    this.version++;
 
    // Subscribe to the item change events (either INotifyPropertyChanged or xxxChanged).
    this.WirePropertyChangedEvents(list[listIndex]);
 
    // If the add is the result of AddNew(), don't sort.  The item will be put into the sort when the add is committed with
    // EndNew() or IEditableObject.EndEdit().
    if (IsPendingNewItem(listIndex))
    {
        this.AppendNewItemToSortIndexes(listIndex);
 
        this.QueueEvent(new ListChangedEventArgs(ListChangedType.ItemAdded, listIndex));
        return listIndex;
    }
    else
    {
        // Sort and filter.
        this.RebuildSortIndexes();
 
        int sortedIndex = this.GetSortedPositionOfListIndex(listIndex);
 
        this.QueueEvent(new ListChangedEventArgs(ListChangedType.ItemAdded, sortedIndex));
        return sortedIndex;
    }
}

In the above code, note that sorting and filtering are not performed as a result of a call to AddNew().  The new item appears as the last item in the view, and is visible even if the filter would normally exclude it.  This allows the user to enter all data in the row before it is relocated by the sort or hidden by the filter.

If the user should change the sort before committing the new row, we just abandon the new row with the equivalent of a call to CancelNew().  Finally, changes to list item properties are not raised as ListChanged events if the changed item is the newly added, uncommitted list item.

private void OnItemChanged(int listIndex, string propertyName)
{
    // Don't re-sort, filter, or raise change notifications for an uncommitted new row.
    if (!this.IsPendingNewItem(listIndex))
    {
        OnItemChangedCore(propertyName);
 
        int index = this.GetSortedPositionOfListIndex(listIndex);
        QueueEvent(new ListChangedEventArgs(ListChangedType.ItemChanged, index));
    }
}

This prevents a sort or filter operation from being triggered. 

Using ObjectListView with the New Row

There's nothing special to do, other than setting DataGridView.AllowUserToAddRows to true.  If you want to see an example in code, I've updated the Master/Details demo (included in the ObjectListView.zip download) to optionally use the new row.

Happy Holidays and Happy Viewing!

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Code-Based Filtering in ObjectListView

Sunday, 17 December 2006 20:39 by jesse

Note: You can download the complete implementation here.

In previous posts, I presented ObjectListView, my implementation of a DataView-like class for arbitrary business objects.  It allows you to bind a DataGridView (and other controls) to an IList of your choice.  As an IBindingListView implementation, ObjectListView supports the Filter property, which allows you to filter the underlying list to present only the items that match the filter expression.

The Filter property allows you to specify simple expressions of the form

propertyName op value

such as "CustomerName = 'Jones'" or "LastVisit > 12/1/2006".

This is all well and good, except for properties that don't lend themselves easily to IComparable-style comparison.  How would one compare a byte array in a meaningful way, for example?  What if you want to filter on a property type that doesn't implement IComparable?

To overcome these obstacles, I've added the FilterPredicate property to ObjectListView.  FilterPredicate is simply a delegate of type ObjectListView.ListItemFilter, which looks like this:

public delegate bool ListItemFilter(object listItem);

The method referred to by the delegate takes a list item, and returns true if the item should be presented, and false if it should not.  This allows you to apply a code-based filter very easily:

string[] states = { "WA", "OR", "CA" };
List<string> westernStates = new List<string>();
westernStates.AddRange(states);
 
view.FilterPredicate = delegate(object listItem)
{
    Customer c = (Customer)listItem;
    return westernStates.Contains(c.Region);
};

The Filter property can still be used as well, if it is more convenient.  If both Filter and FilterPredicate are applied, the last one specified wins.  To remove a filter, set either Filter or FilterPredicate to null.

Enjoy!

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Custom sorting in the DataGridView

Sunday, 10 December 2006 19:42 by jesse

How do I control the sort order for a column in the DataGridView?

This question comes up when the "natural" order of the sorted data is at odds with the user's expectation.  Let's say that you have a customer table with a customer program column.  This column indicates the customer rewards level for a loyalty program.  For our example, the possible values might be:

  • None
  • Silver
  • Gold
  • Platinum

If these values are represented as strings, the order in an ascending sort will be:

  • Gold
  • None
  • Platinum
  • Silver

But our users will expect an order like this:

  • None
  • Silver
  • Gold
  • Platinum

In other words, ordered by rank.

The DataGridView knows nothing of this ranking, of course.  The default sorting scenario (automatic sorting for a data-bound column) of invoking DataGridView.Sort() via a column header mouse click simply delegates to the IBindingList implementation of ApplySort() in the list you've bound to the grid.  Normally, this list would be a DataView.  Using my ObjectListView implementation, this would be a view of a list of arbitrary business objects.  Either way, you end up comparing the properties of the items in the list using the IComparable implementation of the property type.

DataGridView.Sort

What about Sort()?  There's an overload that allows you to provide an IComparer.  Aha!  Well, sort of.  This call doesn't support the data-bound scenario; if you've set the DataSource on the grid, Sort(IComparer) will throw an InvalidOperationException().  Meeting the custom sorting requirement then leads you to unbound or virtual mode.  I haven't worked with virtual mode, so I won't comment on it other than to say that it looks mighty complicated.  Virtual mode provides the programmer with very fine-grained control over the interaction between the grid and the data, at the expense of adding significant complexity to your model.  There are some good examples in the MSDN documentation if you decide to go down this path.

In unbound mode, the SortCompare event is raised by the grid for each item comparison that needs to be done during a sort operation.  This allows you to implement custom sorting.  But of course, this only works for unbound mode - meaning that you can't use a DataSource.

The ObjectListView Solution

Since there's no custom sorting mechanism for the mainstream DataGridView bound mode scenario, I decided to add support for custom sorting in ObjectListView.  ObjectListView is my implementation of a DataView-like class for arbitrary business objects.  It allows you to bind a DataGridView (and other controls) to an IList of your choice.  The ObjectListView can then be sorted or filtered as needed, without any impact on the underlying data.  Your list items are not rearranged or removed during sorting and filtering; only the view changes.

My original sorting mechanism relied on the IComparable implementation of each property in the sort, much like the sorting in a DataView.  To supplement this, I've added the PropertyComparers collection to ObjectListView.  To perform custom sorting, you simply add an IComparer to the collection for the property or properties you're sorting on.

private void radioButtonSortProgram_CheckedChanged(object sender, EventArgs e)
{
    if (this.radioButtonSortProgramAlpha.Checked)
        this.view.PropertyComparers["Program"] = new CustomerProgramComparerAlpha();
    else if (this.radioButtonSortProgramRank.Checked)
        this.view.PropertyComparers["Program"] = new CustomerProgramComparerRank();
    else
        this.view.PropertyComparers.Remove("Program");
}

Given an instance of ObjectListView (this.view), the above code shows how one could change the sorting strategy for a column on the fly.  The items in the list bound to the DataGridView each have a property named Program.  Depending on the radio button selected, the ObjectListView will use a CustomerProgramComparerAlpha, a CustomerProgramComparerRank, or the default IComparable implementation of the Program property type to compare items during a sort.  The comparer classes implement IComparable:

public class CustomerProgramComparerAlpha : IComparer
{
    public int Compare(object x, object y)
    {
        return string.Compare(x.ToString(), y.ToString());
    }
}
public class CustomerProgramComparerRank : IComparer
{
    public int Compare(object x, object y)
    {
        CustomerProgram p1 = (CustomerProgram)x;
        CustomerProgram p2 = (CustomerProgram)y;
 
        if (p1 == p2)
            return 0;
 
        switch (p1)
        {
            case CustomerProgram.Platinum:
                return 1;
            case CustomerProgram.Gold:
                return (p2 == CustomerProgram.Platinum) ? -1 : 1;
            case CustomerProgram.Silver:
                return (p2 == CustomerProgram.None) ? 1 : -1;
            default:
                return -1;
        }
    }
}

The CustomerProgramComparerAlpha implementation just compares the string representation of the two property values.  CustomerProgramCompareRank compares the values using the ordering that the user would expect to see.

The code above is part of the larger Master/Details example in the ObjectListView download.

Details

The PropertyComparers property exposes a PropertyComparersCollection, which is a dictionary of property name keys and IComparer values.  You can replace the IComparer for a property by using the PropertyComparersCollection.Add() method or the indexer (e.g. view.PropertyComparers["PropName"] = myComparer).  To revert to the default IComparable sorting, use the PropertyComparersCollection.Remove() method or set the IComparer value to null via the indexer.

If an IComparer is added to the PropertyComparers collection, it will be used for all subsequent sorts until it is removed from the collection or it is replaced with another IComparer.  If the ObjectListView is already sorted when an IComparer is added to or removed from PropertyComparers, the view will automatically be re-sorted.

If you want to change multiple property comparers after the view is sorted, you can use the ObjectListView BeginUpdate() and EndUpdate() methods to suppress the ListChanged and Sort events until all of the IComparers have been changed.  This prevents multiple refreshes of the DataGridView.  If the ObjectListView is not sorted at the time IComparers are added or removed, no automatic re-sorting is done.

Note that when sorting on multiple columns, a custom IComparer can be used with one sort property, and the default IComparable sort on another.

What's Next?

Since we're headed down the track of custom this-and-that, I think it would be cool to allow filtering through a user-supplied predicate, rather than specifying filter conditions in a string (e.g. view.Filter = "Program > Silver").  A string filter expression syntax must be applicable to a potentially wide variety of property types, and must be somehow boiled down to an IComparable implementation.  This implies that type conversion from a string to the target type is supported.  There is also an implicit constraint that each property type must implement IComparable.  With a custom predicate, you could add your own code to specify whether or not a particular list item should be included in the filter, without the need to support IComparable or string type conversion in the property type.  Look for a custom filter implementation next weekend.  Until then, happy sorting!

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

ObjectListView Demo: Master/Details

Sunday, 3 December 2006 21:04 by jesse

In the latest release of ObjectListView, I included a new demo that illustrates some fine points in real-world data binding with DataGridView.  Namely, binding a list (in this case our ObjectListView) to the grid and binding the current list item (i.e. current grid row) to some other data entry controls.  I also wanted to show how to manage the current grid row position as the list is sorted, and how to do multiple column sorts with the DataGridView.

For this demo, I created a simple but realistic data entry form for customer data:

The grid is bound to an ObjectListView over a list of customers.  The textboxes at the top of the form are bound to different properties of the current list item.  You can edit customer data through the textboxes and by clicking in a grid cell.  The New button adds a new customer to the list.  You can load and save the customer list to a file with the File / Load and File / Save menu options.  Finally, you can change the current list item by clicking on a row in the grid, or by using the navigation controls at the bottom of the form.

Conceptually, this is all very straightforward, but I want to zoom in on a few data binding details that can make a big difference in the user experience.

 Performance

Consider what happens when we load the list of customers from a file.  First we clear the list, then we'll add each customer item one by one.  That means we have at least as many ListChanged events raised as items being added to the list.  Add one for the reset when the list is initially cleared.  If you have event handlers doing anything when ListChanged is raised, your code could end up being very busy.

To ameliorate this situation, ObjectListView supports the BeginUpdate() and EndUpdate() methods.  Users of the WinForms ComboBox, ListBox and ListView controls will find these very familiar.  BeginUpdate() suppresses any ListChanged events until EndUpdate() is called.  If there were list changes that should have resulted in a ListChanged event, EndUpdate() will raise a single ListChanged event with a ListChangedType of Reset.  Here's the code in the demo:

Stream strm = null;
 
try
{
    strm = File.OpenRead("customers.dat");
    BinaryFormatter fmtr = new BinaryFormatter();
    BindingList<Customer> newCustomers = (BindingList<Customer>)fmtr.Deserialize(strm);
 
    this.view.BeginUpdate();
 
    this.customers.Clear();
    foreach (Customer c in newCustomers)
        this.customers.Add(c);
 
    this.view.EndUpdate();
}
catch (IOException)
{
}
finally
{
    if (strm != null)
        strm.Close();
}

Multi-column Sorting

You may have noticed that with the SortMode of your DataGridViewColumns set to Automatic, the DataGridView supports sorting on only one column.  Both ObjectListView and DataView support multiple sort columns.  How do we enable multiple-column sorting in DataGridView?

First, what do we want the user experience to be?  The third-party grids that I've used support multi-column sorting with keyboard modifiers.  When a grid column header is clicked with the mouse, the Shift and Control key states control how that column participates in the sort.

No key modifier: all columns are removed from the sort, except the clicked column.  If the clicked column was already sorted, the sort order is reversed.

Shift key pressed: columns already in the sort stay in the sort.  The clicked column is added to the sort.  If the clicked column was already sorted, the sort order for that column is reversed.

Control key pressed: columns already in the sort stay in the sort.  The clicked column is removed from the sort.  If the clicked column was the only column in the sort, the grid is returned to its original unsorted state.

To implement this behavior, we must first set the SortMode on each DataGridViewColumn to Programmatic.  This tells the DataGridView not to perform sorting when the column headers are clicked.  Secondly, we need to add an event handler for the DataGridView ColumnHeaderMouseClick event.  In this handler, we'll determine the list item properties that correspond to the sort columns, and apply the appropriate sort to the ObjectListView (note that this strategy also works when binding to a DataView).  We also have to manually set the sort glyphs in the sort columns.  The sort glyphs are the little triangles in the column headers that indicate the sort direction.  Here's the code:

// When a column header is clicked, update the sort on the view, and manually set the sort glyphs in the DataGridView columns.
// This is required to support multi-column sorting in the DataGridView.  To support single-column sorting, remove this event
// handler and set the sort mode to automatic for each DataGridView column.
private void dataGridView_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e)
{
    bool shiftPressed = (Control.ModifierKeys == Keys.Shift);
    bool ctrlPressed = (Control.ModifierKeys == Keys.Control);
 
    string sortPropName = this.dataGridView.Columns[e.ColumnIndex].DataPropertyName;
    PropertyDescriptor sortProp = TypeDescriptor.GetProperties(typeof(Customer)).Find(sortPropName, false);
 
    List<ListSortDescription> newSorts = new List<ListSortDescription>();
 
    bool clickedColumnInExistingSort = ctrlPressed;
    foreach (ListSortDescription desc in this.view.SortDescriptions)
    {
        if (desc.PropertyDescriptor.Name == sortPropName)
        {
            if (!ctrlPressed)
            {
                ListSortDirection dir = (desc.SortDirection == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending);
                newSorts.Add(new ListSortDescription(sortProp, dir));
            }
            clickedColumnInExistingSort = true;
        }
        else if (shiftPressed || ctrlPressed)
            newSorts.Add(desc);
    }
 
    if (!clickedColumnInExistingSort)
        newSorts.Add(new ListSortDescription(sortProp, ListSortDirection.Ascending));
 
    this.view.ApplySort(new ListSortDescriptionCollection(newSorts.ToArray()));
 
    foreach (ListSortDescription desc in this.view.SortDescriptions)
    {
        foreach (DataGridViewColumn column in this.dataGridView.Columns)
        {
            if (column.DataPropertyName == desc.PropertyDescriptor.Name)
            {
                column.HeaderCell.SortGlyphDirection = (desc.SortDirection == ListSortDirection.Ascending ? SortOrder.Ascending : SortOrder.Descending);
                break;
            }
        }
    }
}

Retaining the Current Position After a Sort

For me, this is a big one.  By default, after sorting the DataGridView, the current row remains at the same position that it was before the sort.  My expectation is that the current row should move to the row containing the data item that was current before the sort.  This preserves the connection with the data item in any other bound controls besides the grid.  For example, suppose the grid in our demo is sorted on Contact.  If I'm editing the contact name in the textbox above the grid, and tab to the next control, I have a strong expectation that I'm still editing the data for the same customer.  But, if editing the contact name caused the item to change position in the sort, this won't be the case!

We can correct this behavior by handling two events: the PositionChanged event of the BindingSource that the grid is bound to, and the Sorted event of the DataGridView.

Aside:  In this demo, I've tried to construct a realistic scenario.  The BindingSource is used only because the BindingNavigator requires one.  The BindingNavigator gives us the nice VCR-like controls at the bottom of the screen.  Thus, the DataGridView is bound to a BindingSource, which is bound to ObjectListView, which is bound to a BindingList<Customer>.  If the BindingNavigator wasn't needed, we could bind the DataGridView directly to the ObjectListView, and look up the CurrencyManager for the data source and use the PositionChanged event of the CurrencyManager instead.  Whew.

What we want to do is to record the current list item (the customer corresponding to the current grid position) as we navigate among the grid rows.  Then, after the ObjectListView is sorted, we'll find the new position of the formerly current item, and set the position in the BindingSource accordingly:

// Remember the current item as the current grid row changes, so that if the grid is sorted, we can restore
// the current item.
private void bindingSource_PositionChanged(object sender, EventArgs e)
{
    if (this.bindingSource.Position < 0)
        this.current = null;
    else
        this.current = this.bindingSource.Current as Customer;
}
 
// After sorting, move the current grid row to the list item that was current before the sort.
// If the sort mode on the DataGridView columns was automatic, we could handle the DataGridView.Sorted event instead of the view
// event.  Because the sort mode is programmatic, the DataGridView.Sorted event is not raised.
private void view_Sorted(object sender, EventArgs e)
{
    if (this.current != null)
        this.bindingSource.Position = this.bindingSource.IndexOf(this.current);
}

Note that we have to handle the Sorted event of ObjectListView rather than the Sorted event of DataGridView.  This is because we've changed the SortMode of the DataGridViewColumns from Automatic to Programmatic.  In Programmatic mode, we're doing the sorting, not the DataGridView.  Thus, the event is not raised.

Setting the Current Position for a New Item

When adding a new item to the list, the item will be presented in the grid in the correct sorted position.  We want to set the current grid position to the new item, wherever it ends up.  Otherwise, the user has to know to click on the newly added row before starting to enter data in the data entry controls.  This is easily handled in the New button Click event handler:

// Add a new customer to the list, and move the current grid row to the new item.
private void buttonNew_Click(object sender, EventArgs e)
{
    int position = ((ObjectListView)this.bindingSource.DataSource).Add(new Customer());
    this.bindingSource.Position = position;
}

You may ask, why do we even have a New button when we could set the AllowUserToAddRows property on the DataGridView to true, and just let the user enter data directly in the "new" row of the grid?  I don't like that approach because any sorting on the grid changes the position of the new row as soon as data is entered.  With a New button, we can create a new row and set focus on the first data entry control to let the user begin editing.  Because we've already handled the change of the current position after grid sorting, the data entry controls stay correctly bound even if the sort causes the edited new item to change position in the grid.  My preference is to provide data entry ("detail") controls, make the grid read-only, and not allow the "new" row at all.

Designer Experience

If you've used ObjectListView or any object data source to bind to a DataGridView, you might have observed that the designer experience isn't great.  You can't set the DataSource property correctly in the designer view, so you don't get auto-generated columns.

Eventually, I'll add nice design component capabilities to ObjectListView, but for now there's an easy workaround.  In the designer property window, set the BindingSource's DataSource property to the list item type.  Just click on the pull-down menu button and select "Add Project Data Source".

By selecting the object type that will be present in the list (and thus in ObjectListView), the appropriate columns will appear in the DataGridView and you can edit them in design mode to your heart's content.  Elsewhere in your code, you'll need to set the DataSource for the BindingSource back to the ObjectListView.  In the demo, this is done in the form's constructor:

public MainForm()
{
    InitializeComponent();
 
    customers = new BindingList<Customer>();
 
    view = new ObjectListView(customers);
    view.Sorted += new EventHandler(view_Sorted);
 
    this.bindingSource.DataSource = view;
}

What Else?

That wraps it up for the demo.  If there are other ObjectListView scenarios that you'd like to see demonstrated, just let me know.

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

ObjectListView Update (1.0.0.3)

Sunday, 3 December 2006 19:21 by jesse

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();
BindingList<Customer> newCustomers = (BindingList<Customer>)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!

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

DataView for Objects: Final Cut

Sunday, 19 November 2006 21:25 by jesse

 

Note: You can download the complete implementation here.

In my last post I described the implementation of IBindingListView interface in ObjectListView, my implementation of a view for collections of arbitrary objects.  This was the last interface needed to support binding to controls such as the DataGridView.

In this post, I'll show some sample code that demonstrates the use of ObjectListView.

The Demo

I'm going to display data from the Microsoft sample Northwind database in a ComboBox and a DataGridView, showing how to filter and sort with ObjectListView.  Here's the main window:

 

After installing the Northwind database (available here), alter the configuration file Demo.exe.config to reflect your computer name (in place of "DADBOX").  Alternatively, you can modify the connection string in the text box of the demo program main window.

Run demo.exe, set the connection string if needed, and press the Get Data button.  This causes two lists to be populated from the database.  For the demo, I've created two classes, Customer and Order, which do nothing but hold the data from a row in the corresponding database table.  The two lists I'm populating are a List<Customer> and List<Order>.  Then, I create two instances of ObjectListView, binding one to the customers list, and one to the orders list:

private void buttonGetData_Click(object sender, EventArgs e)
{
    Database db = new Database();
    db.ConnectionString = this.textBoxConnectionString.Text;
 
    viewCompanies = new ObjectListView(db.GetCustomers());
    viewOrders = new ObjectListView(db.GetOrders());
 
    this.comboBoxCustomers.DataSource = viewCompanies;
    this.comboBoxCustomers.DisplayMember = "Company";
    this.comboBoxCustomers.ValueMember = "Id";
 
    this.dataGridView.AutoGenerateColumns = false;
    this.dataGridView.DataSource = viewOrders;
 
    this.textBoxFilter.Text = "";
    this.textBoxFilter.Enabled = true;
}

Finally, I set the data source of the company ComboBox to the customers ObjectListView, specifying that the Company property of each Customer will be displayed, and that the Id property will be returned from ComboBox.SelectedValue.  The data source of the grid is set to the orders ObjectListView.  Each grid column is bound through it's DataPropertyName property to a different property of Order.  The column binding code is in designer-generated code.

Now the ComboBox contains a list of all of the customers.  When we select a customer, the orders ObjectListView is filtered to present only the orders for the selected customer:

private void comboBoxCustomers_SelectedValueChanged(object sender, EventArgs e)
{
    if (this.comboBoxCustomers.SelectedValue == null)
        this.viewOrders.Filter = "CustomerId=null";
    else
        this.viewOrders.Filter = "CustomerId='" + this.comboBoxCustomers.SelectedValue.ToString() + "'";
}

Note the usage of CustomerId=null above, to indicate that the CustomerId property of Order should be compared to null when no customer is selected.

Once the grid is populated with orders, you can click on the column headers to sort the orders.  This is done behind the scenes by DataGridView, which delegates to the IBindingList.ApplySort implementation of ObjectListView.

You can also filter the companies displayed in the ComboBox by entering text in the filter TextBox.  As the text is changed, the customers ObjectListView filter is updated:

private void textBoxFilter_TextChanged(object sender, EventArgs e)
{
    this.viewCompanies.Filter = "Company=" + this.textBoxFilter.Text + "*";
}

That's all there is to it!  We've bound lists of arbitrary objects to both a DataGridView and a ComboBox, sorting and filtering our views of the lists without changing the lists themselves, in just a few lines of code.

What's ahead

There's a few things I know want to add to ObjectListView, and I'd like your feedback on other enhancements that you'd like.  Please email me and let me know how you're using ObjectListView and what you think should be added or changed.  In the meantime, I plan on adding:

  • More complex filter expressions (!=, <, >, LIKE, NOT, etc).
  • Support for code-based filter predicates (provide a callback method rather than a filter string expression).
  • BeginUpdate() and EndUpdate() methods to suppress ListChanged events while doing a large number of list insertions/deletions.
  • User-provided comparer for custom sorting.

I've enjoyed building ObjectListView - I hope you find it useful!

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList