Jesse Johnston
.NET Dev. Good times with .NET and coffee

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