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

DataView for objects: Implementation Part IV

Sunday, 22 October 2006 12:30 by jesse

 

Note: You can download the complete implementation here.

In my last post I described the implementation of the IList interface in ObjectListView, my implementation of a view for collections of arbitrary objects.  The goal is to provide the same view capabilities for collections that DataView supports for DataTables.

In this post, we'll move on to the next interface required for data binding, IBindingList.

IBindingList

This interface is the first we've discussed that goes beyond the capabilities of ordinary collections.  IBindingList seems to be designed for the purpose of interacting with a DataGrid or DataGridView control.  In particular, it provides the implementation for the grid column header clicking feature (single column sort), support for the "new" row in the grid (list item construction), and notification of the grid when the collection changes (ListChanged event).

In .NET 1.0 and 1.1, the only implementation of IBindingList was DataView.  Hence, DataView has become the defacto reference implementation of the interface, and the model for the behavior that we need to mimic in ObjectListView.  In .NET 2.0, we also have BindingList<T> and BindingSource.  BindingList<T> is a collection type, which provides no implementation for sorting itself, but rather delegates to a virtual method to be provided in a derived class.  BindingSource is a wrapper for a collection, intended to provide data binding support for collections that do not support binding well.  Neither of these satisfy our need for a view of a collection, where sorting and filtering does not alter the underlying state of the collection

Sorting

IBindingList adds the SupportsSorting property, which indicates whether other sorting methods of the interface are implemented.  This allows a "partial" implementation of IBindingList to still be correct - you could have a non-sorting IBindingList.  If SupportsSorting returns true, the following additional methods and properties must be implemented:

  • ApplySort()
  • RemoveSort()
  • IsSorted
  • SortDirection
  • SortProperty

Of course, these members must be present, even if SupportsSorting returns false (or else your code won't compile), but it is OK to simply throw a NotImplementedException or perform no operation at all in that case.  Callers must always consult SupportsSearching before expecting the other sort members to function correctly.

ObjectListView's ApplySort looks like this:

public void ApplySort(PropertyDescriptor property, ListSortDirection direction)
{
    Lock();
 
    try
    {
        ListSortDescriptionCollection proposed;
 
        if (property == null)
            proposed = new ListSortDescriptionCollection();
        else
        {
            ValidateProperty(property);
 
            ListSortDescription[] props = new ListSortDescription[] { new ListSortDescription(property, direction) };
            proposed = new ListSortDescriptionCollection(props);
        }
 
        if (IsSortDifferent(proposed))
        {
            this.sortProps = proposed;
            ApplySortCore();
        }
    }
    finally
    {
        Unlock();
    }
 
    RaiseListChangedEvents();
}

Here we take a property and direction as parameters.  The PropertyDescriptor for a given property of a type can be obtained from one of the GetProperties() methods of TypeDescriptor.  For example:

PropertyDescriptorCollection props = TypeDescriptor.GetProperties(typeof(SimpleClass));
this.view.ApplySort(props["SomeProperty"], ListSortDirection.Ascending);

props["SomeProperty"] obtains the PropertyDescriptor for the property named SomeProperty of type SimpleClass.

The call to ValidateProperty insures that the specified property is a property of the list item type  It also checks to make sure that the type of the property supports IComparable.  We're imposing this constraint so that we will have a known way to compare the property values when we sort.

After we also determine that the proposed sort property differs from the sort already in place (if any), we proceed to the real work in ApplySortCore:

private void ApplySortCore()
{
    // Invalidate enumerators.
    this.version++;
 
    if (this.SortIndexesDirty)
        this.RebuildSortIndexes();    // sorts and filters
    else
        this.GetSortIndexes().Sort(CompareItems);
 
    this.QueueListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
}

Here we sort our list of indexes, or rebuild them entirely.  Recall that the list of indexes is just a list of positions in the underlying list.  We use the method CompareItems to compare any two list positions to see which should come first in the index list:

private int CompareItems(int x, int y)
{
    object first = this.list[x];
    object second = this.list[y];
 
    if (first == null)
    {
        if (second == null)
            return 0;
        else
            return -1;
    }
    else
    {
        if (second == null)
            return 1;
        else
        {
            for (int i = 0; i < this.sortProps.Count; i++)
            {
                ListSortDescription desc = sortProps[i];
                object firstValue = desc.PropertyDescriptor.GetValue(first);
                object secondValue = desc.PropertyDescriptor.GetValue(second);
 
                int result;
                if (firstValue == null || secondValue == null)
                {
                    if (firstValue == null)
                        result = (secondValue == null) ? 0 : -1;
                    else
                        result = 1;
                }
                else
                    result = ((IComparable)firstValue).CompareTo(secondValue);
 
                if (result != 0)
                {
                    if (desc.SortDirection == ListSortDirection.Descending)
                        result *= -1;
                    return result;
                }
            }
 
            return 0;
        }
    }
}

Here we obtain the list items at the two specified positions x and y.  Then we get the values of the sort property for each of the two list items, and compare the values using their IComparable implementation.  If the sort direction specified in ApplySort was descending, we reverse the return value.

I noted in ApplySortCore() we might rebuild the indexes entirely, instead of sorting them.  Why would we do this?  Simply because our list of indexes might be inconsistent with the underlying list.  Conceptually, this would be because the list isn't informing ObjectListView of additions to or deletions from the list.  Thus, we have an incorrect number of list positions in our index list.  We'd prefer the list to always notify us of changes to the list, including changes to the list items themselves.  List item changes are important when ObjectListView is sorted and the sort property value of an item changes.  At that point the position of the item in the sort may change.  How would we be notified of such changes?

ListChanged

The IBindingList interface introduces an event named ListChanged for exactly this purpose.  Collections implementing IBindingList must raise ListChanged when a list item is added, deleted, moved, or changed.  The ListChanged event is of the delegate type ListChangedEventHandler:

public delegate void ListChangedEventHandler(object sender, ListChangedEventArgs e);

ListChangedEventArgs includes the ListChangedType property, which tells us what was done to the list (add, delete, etc).  The NewIndex property specifies the position of the affected item in the list, and OldIndex specifies the original index if the item was moved.  It's interesting to note that when we receive a ListChanged event for ListChangedType.ItemDeleted, the item has already been removed from the list.  Also, there is a ListChangedType.Reset, which indicates that a "bulk" change has been made to the list.  This is a convenient way of describing an operation like Clear().  Suppose that the list contained 100 items, and Clear was called.  Would a caller prefer 100 delete notifications or 1 reset notification?  I'd prefer the reset.  In any case, our response to a reset must assume that the entire list has changed.

When the underlying list is IBindingList, ObjectListView adds an event handler for the list's ListChanged event, and can easily keep it's index list up to date when the list changes.  Since ObjectListView itself implements IBindingList, we must also raise our own ListChanged event when we receive a ListChanged event from the underlying list.

As with ApplySort() and SupportsSorting, there is a SupportsChangeNotification property of IBindingList that is paired with the ListChanged event.  ListChanged must be raised only if SupportsChangeNotification returns true.  Conversely, if SupportsChangeNotification returns false, the DataGridView will not even subscribe to the ListChanged event.

One of the ListChangedTypes provided in a ListChanged event is ListChangedType.ItemChanged.  An item change could mean one of two different things.  Either an item at the specified list position has been replaced with another item, or a property of the item has a new value.  If a property changed, the PropertyDescriptor property of the ListChangedEventArgs will be set to the appropriate value.  But how would the underlying list know that a property had changed on one of it's items?

INotifyPropertyChanged

In .NET 2.0, the INotifyPropertyChanged interface was added to allow an object to indicate that one of it's property values has changed.  This interface consists of only one member, the PropertyChanged event.  This event is of the delegate type PropertyChangedEventHandler:

public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);

PropertyChangedEventArgs specifies the name of the property that was changed.

By subscribing to each list item's PropertyChanged event, the list can be aware of all item changes, and reflect those through it's own ListChanged event.

Aside:  In .NET 1.x, only DataView implemented IBindingList, so when the ListChanged event was raised, an ItemChanged action was understood to mean a change to a value in a DataRow (i.e. an item property change).  The concept of replacing one row with another row makes less sense in a DataTable than it does in other collections.  The MSDN documentation on ListChanged and ListChangedType.ItemChanged is ambiguous on this point.  Additionally, .NET 2.0 adds an interface IRaiseItemChangedEvents, which allows a collection to explicitly state that it raises ListChanged when a property of a list item changes.  However, even if IRaiseItemChangedEvents is not implemented, the list may raise ListChanged when an item property changes.  DataView does this.

In .NET 1.x (before INotifyPropertyChanged existed), the convention for list items to indicate that one of their property values had changed was to provide a "Changed" event for each property.  So, for a property named Color, the presence of an event named ColorChanged would indicate that the object will raise the event when the Color property changes.

Consequences of not providing change notifications

Let's think about what would happen if a list was bound to a DataGrid, but did not properly support change notifications.  If the list does not raise the ListChanged event when items are added or removed from the list, items added to the list will not appear in a DataGrid, and items removed will continue to be displayed.  If the list does not reflect item property changes by raising ListChanged, changes to items will not be reflected in the DataGrid.  Likewise, if the list items themselves fail to raise PropertyChanged, the list won't know to raise ListChanged and the DataGrid won't reflect changes to the list items.

Although I've designed ObjectListView to work with any IList type of underlying collection, binding scenarios that require the control to be updated when the list changes will only work if the list supports ListChanged and the items provide property change events.  To support .NET 1.x collections, I look for propertyNameChanged style events on the items.  I also look for a ListChanged event on the list type, even if the list does not implement IBindingList.  If the list does not provide a ListChanged event, ObjectListView will still report list changes to bound controls if the list modification is performed through the methods of ObjectListView, rather than through the list methods directly.

For best results, use a list type that implements (and fully supports) IBindingList, and list item types that implement INotifyPropertyChanged.  The easiest list type to use is BindingList<T>, which provides list change notification out of the box, and converts item property changes (INotifyPropertyChanged only) to ListChanged events.

AddNew

An interesting feature of IBindingList is it's support for the "new row" in the DataGrid.  This is the empty row where the user can start typing new data.  It's assumed that a new list item is being manufactured somehow and then added to the list.  The mechanism is the AddNew() method of IBindingList.  Let's look at ObjectListView's implementation:

public object AddNew()
{
    Lock();
 
    ObjectView wrapper = null;
 
    try
    {
        if (!this.allowNew)
            throw new DataException("AllowNew is set to false.");
 
        object newItem = OnAddingNew();
 
        if (newItem == null)
        {
            if (this.itemType == null)
                throw new InvalidOperationException("The list item type must be set first.");
 
            newItem = Activator.CreateInstance(this.itemType);
        }
        else
        {
            if (this.ItemType == null)
                this.ItemType = newItem.GetType();
            else if (newItem.GetType() != this.ItemType)
                throw new ArgumentException("Added item type is different from list item type.");
        }
 
        wrapper = new ObjectView(newItem);
 
        // If the item type is IEditable object, newly added items don't go into the list until EndEdit is called on the item.
        // However, ListChanged is still raised.
        if (this.isEditableObject)
        {
            // If an item was previously added with AddNew() but has not yet been committed with item.EndEdit(), commit it now.
            if (newItemPending != null)
                FinishAddNew();
 
            this.newItemPending = wrapper;
            ((IEditableObjectEvents)this.newItemPending).Ended += new EventHandler(editableListItem_Ended);
            ((IEditableObjectEvents)this.newItemPending).Cancelled += new EventHandler(editableListItem_Cancelled);
 
            this.QueueListChanged(new ListChangedEventArgs(ListChangedType.ItemAdded, -1));
        }
        else
        {
            int index = this.Add(newItem);
        }
    }
    finally
    {
        Unlock();
    }
 
    RaiseListChangedEvents();
 
    return wrapper;
}

There are a few interesting things going on here.  First, we call OnAddingNew() to raise the AddingNew event.  This gives the ObjectListView user an opportunity to provide a newly created list item.  If an item is not provided, we need to create one.  Without baking in special knowledge of the concrete list item type, we must resort to creating an instance with the item type's default constructor.

Once a new list item is created or obtained from the user code, we create an ObjectView wrapper for it.  The purpose of this class is to capture editing events if the list item type implements IEditableObject.

The IEditableObject interface indicates that changes can be made to an object, and then committed or cancelled.  Typically such an object will retain it's original data (before IEditableObject.BeginEdit() was called), and a copy of the data that is being edited.  When IEditableObject.EndEdit() is called, the edited data takes the place of the original data.  If CancelEdit() is called instead, the edited data is discarded.

In the AddNew() method, the addition of the list item is handled in a special way for types that implement IEditableObject.  The ListChanged event must be raised when AddNew() is called (with an ItemAdded ListChangedType), and then again when EndEdit() is called on the list item.  If CancelEdit() is called on the list item instead of EndEdit(), the addition of the item is cancelled.  Between the call to AddNew() and EndEdit()/CancelEdit(), the added item must be kept in a pending state, and not added to the underlying collection.

Following the reference model of DataView, we also commit such a pending added object if AddNew() is called again before EndEdit().

The puzzler here is how ObjectListView would know when EndEdit() or CancelEdit() is called on the list item.  IEditableObject does not include any events to indicate that these actions have taken place!  My solution is to provide the ObjectView wrapper.  ObjectView exposes the newly created list item, and provides it's own IEditableObject implementation, which delegates to the list item, and also raises events that we can monitor.  Here I am using the strategy offered by DataView, which returns a DataRowView from it's AddNew() implementation, rather than the item type of DataRow.  It does this for the same reason, which is to be informed when EndEdit() or CancelEdit() is called on the row.

Other Goop

IBindingList also includes a Find() method which takes a PropertyDescriptor and value as parameters.  There are the SupportsXYZ properties, which specify whether the corresponding method of IBindingList is really implemented.  The AllowEdit, AllowNew, and AllowRemove properties allow the caller to constrain what actions may be taken through the IBindingList implementation.  These are used in conjunction with the bound control; when the DataGrid.AllowNew property is set to false, the new row is hidden, and the AllowNew property of the bound list is set to false.

Finally, there is AddIndex() and RemoveIndex(), which add and remove PropertyDescriptors from indexes used for searching.  These methods are not well documented and suggest that the implementations can be a "nonoperation".  Not having a clear use case for these methods, I took the advice in the documentation.

Where are we?

I've described the IBindingList implementation of a collection view class.  This is by far the most important interface for a view to support.  It supports change notification, so that bound controls can be kept synchronized with the underlying list.  Single property sorting is also provided.

The features still missing are filtering and multi-property sorting.  In my next post, I'll walk through the last interface, IBindingListView, which adds these features.

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

DataView for objects: Implementation Part III

Sunday, 15 October 2006 16:57 by jesse

Note: You can download the complete implementation here.

In my last post I described the implementation of the ICollection interface in ObjectListView, my implementation of a view for collections of arbitrary objects.  The goal is to provide the same view capabilities for collections that DataView supports for DataTables.

In this post, we'll move on to the next interface required for data binding, IList.

Positional list access

The IList interface defines the methods and properties that make a list-style collection fully functional.  We can add and remove items, and importantly for our view, add, remove, and access items by position.  When we implement these positional methods, the position will always be interpreted as the position within the view, rather than the position in the underlying list.  In our view implementation, we'll keep a reference to the original underlying list:

private IList list;

As an example of this positional access, lets say that list contains these items:

If we were to access list[3], we would see G.

Now let's say we have our view of the list, sorted and filtered to exclude C.  The view presents itself as a list that looks like this:

If we access view[3], we expect to obtain E.

With that in mind, we know that we will need a sorted, filtered store of list items, and we know that we will need to access it by view position.

For my sorted, filtered store, I chose to maintain a list of integers, such that this list is in sorted order, and the values in the list represent indexes of values in the underlying collection.

private List<int> sortIndexes = new List<int>();

For the previous example, sortIndexes looks like this:

 

You can see that these indexes map to the values in the underlying list.  To obtain the value at view[3], we look up the value stored in sortIndexes[3].  That value is 6, so we go to list[6] for the value to return.

To keep track of the sorted and filtered positions, we implement a few private methods.  RebuildSortIndexes() re-creates the list of sorted indexes for the current list contents.  GetListPositionOfViewIndex() retrieves the position in the list that corresponds to the given view index (i.e. the method that returns 6 for the index 3 in the above picture).  GetSortedPositionOfListIndex() does the converse - given an index into the underlying list, it finds the corresponding index in the view.  GetSortIndexes() provides access to the list of sorted indexes, rebuilding them if needed.

private int GetSortedPositionOfListIndex(int listIndex){    if (this.IsSorted || this.IsFiltered)        return this.GetSortIndexes().IndexOf(listIndex);    else        return listIndex;}private int GetListPositionOfViewIndex(int viewIndex){    if (this.IsSorted || this.IsFiltered)        return this.GetSortIndexes()[viewIndex];    else        return viewIndex;}private List<int> GetSortIndexes(){    // Items have been added or removed through the list directly, instead of through the ObjectListView.    if (this.SortIndexesDirty)        RebuildSortIndexes();    return this.sortIndexes;}private void RebuildSortIndexes(){    this.sortIndexes = new List<int>(this.list.Count);    if (this.IsFiltered)    {        for (int i = 0; i < this.list.Count; i++)        {            if (this.filterPredicate(this.list[i]))                this.sortIndexes.Add(i);        }    }    else    {        for (int i = 0; i < this.list.Count; i++)            this.sortIndexes.Add(i);    }    if (this.IsSorted)        ApplySortCore();}

We'll discuss the filtering and sorting implementations later, so for now, just know that IsFiltered and IsSorted indicate whether or not filtering and sorting have been specified.  If the view is filtered, the filterPredicate is used to test each list item for inclusion in the list of indexes.  After adding all of the indexes to the sortIndexes list, the sortIndexes list is sorted.

The GetSortIndexes() wrapper around sortIndexes is used instead of directly referring to sortIndexes, so that we have a point to determine if the indexes are invalid, and regenerate them if needed.  Normally, the list will notify the view of list item additions and removals.  Lists that do not implement IBindingList do not provide this notification however.  To better support these lists, the view compares the size of the sortIndexes list with the size of the list.  If different, the indexes need to be rebuilt.  This check is implemented in the view's SortIndexesDirty property:

private bool SortIndexesDirty{    get    {        // The capacity of the indexes list is the size of the list as the view knows it.        // If the size of the list has changed (items added or removed), we know that the indexes are now invalid.        // This can happen if the list is manipulated directly (not through methods of the view) and the list does not        // notify the view of the changes.        Lock();        bool dirty = (sortIndexes.Capacity != this.list.Count);        Unlock();        return dirty;    }}

Adding items

The methods of IList provide a few different ways to add and remove items.  Rather than cover every line of the implementation, I'll just discuss the Add() method in detail and touch on the interesting points of the others.  Here's Add():

public int Add(object value){    Lock();    int sortedIndex = -1;    try    {        if (!this.allowNew)            throw new DataException("AllowNew is set to false.");        if (this.list.Count == 0 && value != null)            this.ItemType = value.GetType();        int index = this.list.Add(value);        // If the list supports ListChanged, OnItemAdded will have already been called.        if (!this.supportsListChanged)            sortedIndex = this.OnItemAdded(index);        else            sortedIndex = this.GetSortedPositionOfListIndex(index);    }    finally    {        Unlock();    }    RaiseListChangedEvents();    return sortedIndex;}

Here we're just adding an item to the underlying list.  We get the index of the added item from the list, and translate that to the sorted index in the view.  The view's index is the one we want to return, as all positional interaction with the view is done in terms of the sorted and filtered position.

If this is the first item added to the list, we need to look at the list item type and it's capabilities.  The ItemType property setter does this.

public Type ItemType{    get    {        Lock();        Type t = this.itemType;        Unlock();        return t;    }    set    {        if (value == null)            throw new ArgumentNullException("ItemType");        Lock();        try        {            if (itemType != null && itemType != value)                throw new InvalidOperationException("The list already contains items");            this.isEditableObject = typeof(IEditableObject).IsAssignableFrom(value);            this.supportsNotifyPropertyChanged = typeof(INotifyPropertyChanged).IsAssignableFrom(value);            this.itemType = value;            this.itemProperties = TypeDescriptor.GetProperties(value);            this.itemPropertyChangedEvents = this.GetPropertyChangedEvents(value);            this.supportsPropertyChangedEvents = this.itemPropertyChangedEvents.Count > 0;        }        finally        {            Unlock();        }    }}

With respect to the list item type, we need to find out if the item will raise events when it's property values change, and whether it provides the "undo-able" editing mechanism of IEditableObject.  If the item type implements INotifyPropertyChanged, we know that it will raise PropertyChanged when the value of any public property changes.  This interface is new to .NET 2.0.  In previous versions, the convention for property change notification was the existence of an event of the same name as the property, plus "Changed".  For example, if the property name was "AccountId", the expected change event would be called "AccountIdChanged".  If the event did not exist, there was no notification.  In the ItemType property setter, we check for both INotifyPropertyChanged and .NET 1.x style property change events.

Back in the Add() method, we call OnItemAdded() if the underlying list doesn't support ListChanged.  I mentioned earlier that lists implementing IBindingList raise an event when items are added or removed.  When our IBindingList list raises ListChanged for a newly added item, we call OnItemAdded() in an event handler elsewhere.  Here we're handling the case where list is not an IBindingList.

private int OnItemAdded(int listIndex){    // Invalidate enumerators.    this.version++;    // Subscribe to the item change events (either INotifyPropertyChanged or xxxChanged).    this.WirePropertyChangedEvents(list[listIndex]);    // Sort and filter.    this.RebuildSortIndexes();    int sortedIndex = this.GetSortedPositionOfListIndex(listIndex);    this.QueueListChanged(new ListChangedEventArgs(ListChangedType.ItemAdded, sortedIndex));    return sortedIndex;}

Since the contents of the list are changing, we update our version number to inform any enumerators that they are no longer valid (recall that IEnumerable.GetEnumerator() returns an IEnumerator object that knows what version of our view it was created for).  If our ItemType property setter determined that the list item type supports property change events, we need to add event handlers to those events.  We need to update the sort indexes, as the new item might appear anywhere in the sort, and may or may not meet the filter criteria.

The returned index is the view index, which might be -1 if the added item is not visible.  For example, if our filter limits the view to items with a LastName property equal to "Jones", and the added item's LastName is "Smith", then the item will be added to the underlying list, but will not be accessible through the view.  The Insert() method presents a related dilemma, as it specifies a position in the view to insert the added item.  If the view is sorted, the position of the added item is determined by the sort, so the specified position must be disregarded.

Finally, notice the call to QueueListChanged().  Because our view is implementing IBindingList, it is expected to raise the ListChanged event just like any collection that implements IBindingList would.  If our underlying list is an IBindingList, then the list will raise the event, and the view will also raise the event.  Why are we "queuing" the ListChanged notification instead of just raising the event?  Looking back at the Add() method, we see that OnItemAdded() is called while a lock is held.  The lock is held if the underlying list is synchronized; if not, Lock() and Unlock() are no-ops.  Consider what would happen if a lock is held while ListChanged is raised.  If the user's event handler is on another thread and calls a method or property of the view (e.g. view.Count), it will deadlock.  To prevent this, we queue up any events that would normally be raised while the lock is held, and then raise them after releasing the lock.

Where are we?

We've added support for the IEnumerable, ICollection, and IList interfaces, which expose the basic features of a list-style collection.  In our IList implementation, we're providing the necessary translation from index positions in the view to index positions in the list, and vice versa.  To satisfy the requirements of IBindingList, we're raising the ListChanged event when items are added to or removed from the view.

In my next post, I'll describe the IBindingList interface, where sorting and searching behaviors are exposed.  I'll also discuss more issues surrounding the ListChanged event.

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

DataView for objects: Implementation Part II

Thursday, 12 October 2006 00:57 by jesse

 

Note: You can download the complete implementation here.

In my last post I described the implementation of the IEnumerable interface in ObjectListView, my implementation of a view for collections of arbitrary objects.  The goal is to provide the same view capabilities for collections that DataView supports for DataTables.

In this post, we'll move on to the next interface required for data binding, ICollection.

ICollection

This interface adds seemingly very little behavior to our view.  There is the Count property, which returns the number of items in the collection, and the CopyTo method, which copies items from the collection to an array.  We could delegate both of these to the corresponding implementations in the underlying list, but we want to reflect only the items in the list that meet the filter criteria (if any).  Note that the filter is implemented in IBindingListView, so I'm thinking ahead here.  Count will return only the number of items in the list that match the filter.  The same applies to CopyTo; only the matching items will be copied.  A further constraint on CopyTo is that we want it to copy items in their sorted order, if a sort has been applied to the view.  Sorting is also introduced later, in the implementation of IBindingList.  Let's look at the implementation:

public int Count{    get    {        Lock();        try        {            return this.GetSortIndexes().Count;        }        finally        {            Unlock();        }    }}public void CopyTo(Array array, int index){    Lock();    try    {        for (int i = 0; i < this.Count; i++)            array.SetValue(this[i], i + index);    }    finally    {        Unlock();    }}

First, looking at Count, we see that we're returning the count of our sorted indexes.  I'll cover this method in my next post, when we get into the implementation of IList.  For now, assume that we're keeping a separate list of only the filtered items, in sorted order.  Thus, the count we're returning is the count of the items visible through the view.  CopyTo() uses the indexer (this[i]) to retrieve the list items in sorted order; we'll also cover the indexer in the discussion of IList.

What about Lock() and Unlock - why are these needed?

So far, we've made no claim to thread safety.  However, ICollection adds the IsSynchronized and SyncRoot properties, which allow a collection to support thread-safe access.  If the IsSynchronized property of our list is true, the collection is stating that it is thread-safe, and that it's methods will lock on the object returned by SyncRoot when modifying the collection.  In our implementation of these two properties, we can delegate directly to the list properties.  If the list is not synchronized, the view cannot be either.  The question is, if the list is synchronized, does the view need to be as well?  I think the answer is yes.  The view represents a potentially reordered subset of the list, but all other characteristics of the list should be faithfully reflected in the view.  Thus, if the list is thread-safe, we'll extend that behavior to the view.

The Lock() and Unlock() methods provide the locking coordination with the underlying list, if needed.  Here is the locking code:

private void Lock(){    if (this.synced)        Monitor.Enter(this.SyncRoot);}private void Unlock(){    if (this.synced)        Monitor.Exit(this.SyncRoot);}

The value of the synced field is set in our constructor as follows:

this.synced = list.IsSynchronized && list.SyncRoot != null;

We're making the assumption that the thread-safety behavior of the underlying list is invariant over it's lifetime.

One final point to make about the locking behavior is the use of the try / finally blocks in CopyTo() and Count.  If an exception is thrown while the ObjectListView is in a locked state, we want to guarantee that we unlock before propagating the exception.  Because we're locking on the SyncRoot provided by the list, remaining locked after throwing an exception could leave other consumers of the list deadlocked.  We'll see other interesting consequences of locking later in our implementation.

Where are we?

We've implemented the IEnumerable and ICollection interfaces, which expose relatively simple features.  However, we've already identified the need for maintaining a separate list of sorted and filtered items, as well as respecting the thread-safety guarantees of the underlying list.

In my next post, I'll move into the IList interface, where we finally expose list manipulation behavior such as adding and removing items.

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

DataView for objects: Implementation Part I

Monday, 9 October 2006 23:01 by jesse

Note: You can download the complete implementation here.

In my last post, I described the .NET Framework support for views of collections of arbitrary objects.  In short, the required interfaces are provided, but no implementations.  In this post I'll describe what we need to do to roll our own view.

In a nutshell, we need a view class that implements IBindingListView to wrap our collection of objects.  Our view will provide sorting and filtering, and neither of these operations will change the underlying collection.

Prior Art

To start, I'd like to acknowledge the work of others that was invaluable to my understanding (and will be to yours).

Michael Weinhardt wrote a series of articles for MSDN (November 2004 - April 2005) that walk through the basics of custom data binding:

Custom Data Binding, Part 1

Custom Data Binding, Part 2

Custom Data Binding, Part 3

These excellent articles show an in-depth implementation of IBindingListView in a custom collection deriving from BindingList<T>.  Bear in mind that the code in these articles is based on beta versions of .NET 2.0, so in some areas doesn't correspond exactly to the classes in the released version of the .NET Framework.

Rockford Lhotka wrote an article for MSDN (January, 2004) that describes the implementation of a view implementing IBindingList:

Sorting the Unsortable Collection

Finally, Brian Noyes wrote a tremendous book on data binding for .NET 2.0, which includes an implementation of IBindingListView in a custom collection:

Data Binding with Windows Forms 2.0: Programming Smart Client Data Applications with .NET

Brian is a great resource for both data binding and also ClickOnce .NET deployment.  You should check out his blog.

Getting Started

Assumptions.  We know that our view class will essentially be a wrapper around an existing collection of objects.  Further, I'll make the assumption that this collection is homogeneous - every item in the collection is of the same type.  I won't assume that the collection is generic (e.g. List<T>), because I'd like to support simple lists such as ArrayList.  Lastly, I'll assume that the collection is an IList, so that we know it supports index-based access to the items in the collection.  From this point on, I'll refer to the underlying collection as the list, and the objects in the list as the items.  We'll talk about some additional important aspects of the list type and the item type later.

Approach.  Many of the methods of the view delegate to the methods of the list.  Since the view will be implementing IList, we'll be providing a set of methods and properties in the view that duplicate those of our underlying list. When we add items to the view, we'll call list.Add(), and so on.  In fact, in the simple case of a view in an unsorted and unfiltered state, the IList methods could pass directly through to the list and still function correctly.  To work through the details of the implementation, I'm going to implement the various interfaces required for the view, from the simplest to the most complex.  We'll start by implementing these methods in the view simply by calling the same method in the list, and make adjustments when the list fails to meet our needs.

ObjectListView

I'll name our view class ObjectListView.  We'll start with a very basic skeleton:

public ObjectListView(IList list){    if (list == null)        throw new ArgumentNullException("list");    if (!this.IsListHomogeneous(list))        throw new ArgumentException("The list contains multiple item types", "list");    this.list = list;}

For now, just assume that IsListHomogeneous() just checks that all of the items in the list are of the same type.

IEnumerable

Our first task is to implement this foundational behavior of any .NET collection.  IEnumerable contains one method, GetEnumerator().  The object returned by this method captures a position in the list, can return the item at this position, and can be advanced to the next item in the list.

We could start by implementing GetEnumerator() as list.GetEnumerator(), but we know that will be wrong if the view is sorted or filtered.  We want the enumerator to return the items in sorted order, ignoring any items that would be excluded by the filter.  For this task, we need a custom enumerator.  The enumerator will keep a reference to the view, and remember the version of the view that it was created with.  The version is just a counter that allows the enumerator to determine if the collection has changed since the enumerator was created.  If the version changes, the enumerator can no longer reliably continue iterating through the collection.  The rest of the enumeration implementation is straightforward.  Thinking ahead, we know that we will be implementing an indexer for our view, which will provide sequential access to the items in sorted, filtered order.  Thus, our enumerator can delegate it's work back to the view:

private class ObjectListViewEnumerator : IEnumerator{    private int position = -1;    private ObjectListView view;    private int version;    public ObjectListViewEnumerator(ObjectListView view)    {        if (view == null)            throw new ArgumentNullException("view");        this.view = view;        this.version = view.version;    }    public object Current    {        get        {            if (this.version != this.view.version)                throw new InvalidOperationException("The collection has been modified.");            if (this.position > this.view.Count - 1)                throw new InvalidOperationException("The enumerator is past the end of the collection.");            if (this.position == -1)                throw new InvalidOperationException("The enumerator is before the beginning of the collection.");            return this.view[this.position];        }    }    public bool MoveNext()    {        if (this.version != this.view.version)            throw new InvalidOperationException("The collection has been modified.");        if (this.position >= this.view.Count - 1)        {            return false;        }        else        {            this.position++;            return true;        }    }    public void Reset()    {        if (this.version != this.view.version)            throw new InvalidOperationException("The collection has been modified.");        this.position = -1;    }}

Note that ObjectListViewEnumerator is a nested class of ObjectListView, so it has access to the private members of ObjectListView.  The enumerator class is private since we only need to provide an IEnumerator to consumers.  The GetEnumerator() method of ObjectListView then becomes very simple:

public IEnumerator GetEnumerator(){    return new ObjectListViewEnumerator(this);}

Where Are We?

We've implemented IEnumerable, the first of the five key interfaces in our view class.  In my next post, I'll detail the implementation of the next interface, ICollection.

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

DataView for objects: What's in the box

Sunday, 8 October 2006 18:50 by jesse

In my previous post I made the case for having a DataView-style construct for collections of arbitrary objects.  In this post, I'll review what you can (and can't) do to achieve this with the built-in .NET 2.0 Framework classes.

View vs. Collection

First, why use a "view" rather than just a collection?  For the same reason we would use a DataView rather than a DataTable.  Let's say we want to present some customer data in a DataGridView control, filtering a DataTable of customers to only those in Portland, Oregon.  In a second DataGridView, we want to show all of the customers, not just the ones in Portland.  The two grids are binding to the same table.  If the filtering operation of the first grid actually removed rows from the table, the second grid wouldn't be able to show all of the rows.  That's no good.  That's why we use a DataView.  The DataView maintains a sort and filter without changing the contents of the backing DataTable.  In this case, we can just create two DataView objects for the same DataTable, and set the RowFilter on the first to include only customers in Portland.  Voila.

Aside:  In fact, if you bind a DataTable to a DataGridView, you're really binding to the default DataView for that table.  When you set the DataSource property of the DataGridView to a DataTable, the CurrencyManager for the grid obtains the list of items that it should bind to from the IListSource.GetList() implementation of DataTable, which returns a DataView.

The same point applies to our collection of arbitrary objects.  It's not enough for our collection to remove items when it is filtered, or to rearrange it's items when sorted.  If it did, changing the sort on one DataGridView would have the effect of changing the sort on another DataGridView if both were bound to the same collection.

OK, we've established that a custom collection itself doesn't fit the bill.  What support is there for a custom view for our collection?  To start with, let's look at the interfaces that DataView implements:

When an object (like our custom view) is provided as a data source to a DataGridView or other bound control, the data source object's implementations of these interfaces will be used by the control.  If we develop our view to fully support these interfaces, the behavior of the DataGridView with our view should be very close to the control's behavior when used with a DataView.  The DataGridView and other controls don't require all of these interfaces - you could use an IList implementation as a data source.  However, if your data source doesn't support all of the interfaces, the control won't be able to support all of the behavior expected by the user.

Of these, ITypedList, ISupportInitializeNotification and ISupportInitialize primarily support design mode in Visual Studio, and aren't strictly required for our purposes.  IEnumerable, ICollection and IList allow consumers of DataView to use it as if it is a collection - enumerating the items in the view with foreach, add items, remove items, access items by index, and so on.  These are the methods used by DataGridView to determine what items should be displayed in the grid and in what order.

Things start to get more interesting with IBindingList.  The methods and properties of this interface expose sorting and searching behavior.  Additionally, we have properties that restrict manipulation of the collection through the view:  AllowEdit, AllowNew, and AllowRemove.  Finally, one can add a new item to the underlying list without knowing how to construct it, via AddNew.  The Allow* properties of the data source object constrain the behavior of the DataGridView.  AddNew is called when the user enters the "new row" of the grid.  As for the sorting and searching methods, for now, just note that only one property can be specified in a sort or search (i.e., only single-column sorting is supported).

IBindingListView provides the premier binding experience.  A superset of IBindingList (and thus IList, ICollection and IEnumerable as well), it adds multi-property sorting and filtering.  A view class that implements IBindingListView behaves like a list, and supports sorting and filtering over multiple properties - and that's all we need.

So, what classes implement IBindingListView?  DataView and BindingSource.  We know that DataView doesn't support collections of arbitrary objects.  What's BindingSource about?

BindingSource

This fine class doesn't act as a view, so it doesn't help us.  BindingSource is a wrapper over a data source that makes binding to a list data source easier.  It provides indirection between the bound control and the actual data source, allowing the underlying list data source to be changed out on the fly.  It also provides it's own currency management, and is designed to be used with the BindingNavigator component to provide navigation through the data source list.  Although BindingSource implements IBindingListView, it delegates the filtering and searching work to the corresponding methods of the underlying data source.  So, you can set a filter on a BindingSource, but if the data source that it wraps doesn't support filtering, it won't help you.

Where are we?

At the beginning.  There's no implementation of a view for arbitrary objects in the .NET Framework.  It's time to roll up our sleeves and get coding!  In my next installment, we'll do just that.

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList
Tags:  
Categories:   .NET | ObjectListView
Actions:   E-mail | del.icio.us | Permalink | Comments (0) | Comment RSSRSS comment feed

DataView for objects?

Sunday, 8 October 2006 15:56 by jesse

I've often wanted something like the DataView for a collection of my own data objects.  DataView allows you to present a sorted, filtered subset of the rows in a DataTable, without actually changing the underlying table.  You can bind two DataViews of the same table to two different Windows Forms controls, using a different sort and filter in each.  Very groovy.

Unfortunately, DataView only provides this goodness for the DataTable class.  If your data happens to be in a DataTable, no problem.  The DataTable and containing DataSet are quite powerful.  Nevertheless, there are circumstances that lead me to want a view over arbitrary class objects:

  • Having an existing domain model of classes suitable for data binding.

    Typically, the systems I work with have a well-defined object model, and back-end data is represented in terms strongly-typed objects rather than as data rows.  Why should I have to use up memory to duplicate the data in the form of a DataSet, just so that I can bind it to a control?  There's also a cost in performance and development time to do this.


  • Data that can't be represented well in a DataColumn.

    Theoretically, any .NET type can be used in a column of DataRow.  In the DataSet designer, you can enter the name of your own type, and the generated code will properly support it.  I suspect this usage is rare in actual practice, though.  Typically a DataSet is used because it is designed to move data back and forth from a client application to a database.  A user type in a DataColumn would have to map to a UDT in the database, and this requires it's own special handling.  So, it can be done, but if you're populating a DataRow with your own custom types, it begs the question as to why you're using a DataRow at all.  Probably because you want to use the DataView.


  • Data that sorts in an unconventional way.

    I recently ran into a case where I was using a DataColumn to represent one of a few discrete text values that needed to be sorted in a special order (not A-Z or Z-A).  What I really wanted to do was have a column of my own type, and use it's own IComparable implementation.


  • Complex filtering needs

    I love the RowFilter property of the DataView.  It provides a very generalized set of operators that allow you to create a filter criteria expression.  Wonderful.  Sometimes I need more, though.  I'd like to provide my own filtering predicate in code, rather than using the predefined expression operators.

In my next post, I'll review the support that .NET already provides for a filtered, sorted view of a custom collection.  Suffice it to say that we have some work to do.

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList