csammisrun

A rare situation

.NET 3.0 functionality in .NET 2.0: a sorting, filtering, bindable list – Part Two

with one comment

The thrilling conclusion to Part One!

When we left our hero, the BindingListCollection, we had a nicely serviceable collection that could be bound, sorted, filtered, and was .NET 2.0 compliant. New items added into the list were sorted and filtered correctly, and life was wonderful…but it was not perfect yet.

Ch-ch-ch-ch-changes (don’t want to be a static list)

If you have a contact list with more than zero people on it, chances are there are going to be events that alter the state of the list in some way. Contacts change status, occasionally their names change, the number of contacts in a group can change, and so on. A contact list is a highly mutable structure, and we want the data structure containing it to be able to respond dynamically.

I mentioned in part one that the BindingListCollection implements INotifyPropertyChanged, so that anything that binds to it can be made aware when a property on the collection changes. I also mentioned, in an off-hand way, that all good databound classes implement that interface. The core data structures that make up the shaim contact list – ContactGroup and MetaContact – implement INotifyPropertyChanged. Maybe the BindingListCollection should take advantage of this! We’re going to require that anything contained in a BindingListCollection implements INotifyPropertyChanged by putting a restriction on the generic type used by the container.

public class BindingListCollection : BindingList, INotifyPropertyChanged where T : class, INotifyPropertyChanged

Now that we know for a fact that all items being inserted into the list will have the PropertyChanged event, we can update InsertItem to attach event handlers when a new item is added:

...
    // Insert the item into the list
    if (index != -1)
    {
        if (index > base.Count)
        {
            index = base.Count;
        }
        base.InsertItem(index, item);
    }

    INotifyPropertyChanged propitem = item as INotifyPropertyChanged;
    if (propitem != null)
    {
        propitem.PropertyChanged -= item_PropertyChanged; // Don't attach it twice
        propitem.PropertyChanged += item_PropertyChanged;
    }

    // Fire the AddingNew event
    OnAddingNew(new AddingNewEventArgs(item));
...

The event handler is removed in RemoveItem to avoid stray references that prevent garbage collection.

Now let’s think about the event handler method, item_PropertyChanged. The PropertyChanged event has only one parameter: the name of the property that changed on the item that raised the event. We don’t want to muck with the list on every property change, though – this is where the BindingListCollection starts to get specialized for shaim. With a minimal amount of effort, it could be made to work in generic applications.

shaim supports sorting/filtering by a metacontact’s display name (alphabetic sort), a metacontact’s status (status sort, or hide offline contacts), a group’s display name (alphabetic sort), or a group’s count (hide empty groups). With that in mind, we can listen for only those specific properties, and fiddle with the list contents only if one of those changes.

void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    T item = sender as T;

    // If the list isn't sorted or filtered, no rearrangement is necessary
    if (!isSorted && !isFiltered)
    {
        return;
    }

    // Lists of groups can be sorted/filtered by display name and online count,
    // lists of metacontacts by display name and status
    if (e.PropertyName == "DisplayName" || e.PropertyName == "Status" || e.PropertyName.EndsWith("Count"))
    {
        MoveWithinList(item);
    }
}

MoveWithinList: thanks for being terrible, WPF TreeView

Before I start raving incoherently about a bug that took for-freakin’-ever to track down and squash, I shall present and explain the code listing for MoveWithinList and its support methods:

/// <summary>
/// A constant value returned by FindInCurrentList
/// when the requested item is not in the list
/// </summary>
private const int ITEM_NOT_IN_LIST = -1;

/// <summary>
/// Move an item within the list by its current properties
/// </summary>
private void MoveWithinList(T item)
{
    bool fireItemsChangedEvent = false;
    int originalPosition, newPosition;

    // Remove the item at its current position
    originalPosition = FindInCurrentList(item);
    if (originalPosition != ITEM_NOT_IN_LIST)
    {
        Items.RemoveAt(originalPosition);
        fireItemsChangedEvent = true;
    }

    // Reinsert it at its new position, maybe
    newPosition = PlaceByCurrentComparison(item);
    if (newPosition != ITEM_NOT_IN_LIST)
    {
        Items.Insert(newPosition, item);
        fireItemsChangedEvent = true;
    }

    if (fireItemsChangedEvent)
    {
        // Figure out which list changed event to fire, given the change
        // in the active list.

        if (IsValidPosition(originalPosition) && IsValidPosition(newPosition)
            && originalPosition != newPosition)
        {
            // An item was valid, it's still valid but it moved, fire a...something
            // [trombone making that wah-wah-waaaaaah sound]
        }
        else if (IsValidPosition(originalPosition) && !IsValidPosition(newPosition))
        {
            // An item was valid, now it's not, fire a Delete event
            OnListChanged(new ListChangedEventArgs(ListChangedType.ItemDeleted, originalPosition));
        }
        else if (!IsValidPosition(originalPosition) && IsValidPosition(newPosition))
        {
            // An item was invalid, now it is, fire an Add event
            OnListChanged(new ListChangedEventArgs(ListChangedType.ItemAdded, newPosition));
        }

        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("Items"));
        }
    }
}

/// <summary>
/// Returns a value indicating whether the given position is valid for the list
/// </summary>
private static bool IsValidPosition(int position)
{
    return position > ITEM_NOT_IN_LIST;
}

/// <summary>
/// Returns the index of an item in the currently displayed list
/// </summary>
private int FindInCurrentList(T item)
{
    return Items.IndexOf(item);
}

The logic here is pretty simple. When an item’s property changes, one of four things will happen: it may be moved by the current sort, it may become visible if it was previously filtered, it may become hidden by the current filter, or it might do nothing. MoveWithinList locates where the item is currently, where it’s going (if anywhere), does the rearranging, and fires a list changed event to tell binders what just happened. This completes the mimicry of INotifyCollectionChanged.

Almost completes it, that is. Note the comment in the case where the item moves in the currently visible list. This is not as intuitive as one might hope. If you look up the MSDN documentation for the ListChangedType enumeration, you’ll see ItemAdded and ItemDeleted – both of which are already used by MoveWithinList – and also ItemMoved. How simple! In the mystery event area, just put:

OnListChanged(new ListChangedEventArgs(ListChangedType.Move, newPosition, originalPosition));

…and just wait for the horrific exceptions to roll in!

Unexpected collection change action 'Move'.
  at System.Windows.Controls.TreeViewItem.OnItemsChanged(NotifyCollectionChangedEventArgs e)
  at System.Windows.Controls.ItemsControl.OnItemCollectionChanged(
     Object sender, NotifyCollectionChangedEventArgs e)

Oh boy! The WPF TreeView apparently doesn’t support moving items under a single TreeViewItem. I wonder why this is? Let’s visit our good friend, Reflector:

protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
    switch (e.Action)
    {
        case NotifyCollectionChangedAction.Add:
            return;

        case NotifyCollectionChangedAction.Remove:
        case NotifyCollectionChangedAction.Reset:
            if (this.ContainsSelection)
            {
                TreeView parentTreeView = this.ParentTreeView;
                if ((parentTreeView == null) || parentTreeView.IsSelectedContainerHookedUp)
                {
                    return;
                }
                this.ContainsSelection = false;
                this.Select(true);
            }
            return;
    }
    throw new NotSupportedException(SR.Get(SRID.UnexpectedCollectionChangeAction, new object[] { e.Action }));
}

Going back up the call stack of the exception, I see that ListChangedType.ItemAdded gets changed to NotifyCollectionChangedAction.Add, ItemDeleted gets changed to Remove, and Reset stays the same. ItemMoved, which gets changed to NotifyCollectionChangedAction.Move, is simply not supported by WPF’s TreeView. C’est la vie, I suppose, let’s just go with what is supported. Instead of the ItemMoved ListChanged event, do a reset instead:

if (IsValidPosition(originalPosition) && IsValidPosition(newPosition)
    && originalPosition != newPosition)
{
    OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); // <-- f-f-f-frickin hacks!
}

Fortunately this happens less than you might think. Most of the time, a change event happens slow enough that it triggers an add and delete instead of a full reset.

Getting at the real stuff

Consumers of the BindingListCollection may still want to know about what's underneath the filtering and sorting, so we'll expose a couple properties to let them get at that data (but in a read-only sense).

/// <summary>
/// Return the total number of items in the list, visible or not
/// </summary>
public int TotalCount
{
    get
    {
        if (isSorted || isFiltered)
        {
            return unsortedList.Count;
        }
        else
        {
            return Items.Count;
        }
    }
}

/// <summary>
/// Gets a read-only view of the underlying list without filtration or sorting
/// </summary>
public ReadOnlyCollection<T> UnderlyingList
{
    get
    {
        if (isFiltered || isSorted)
        {
            return new ReadOnlyCollection<T>(unsortedList);
        }
        return new ReadOnlyCollection<T>(Items);

    }
}

To finish up, we still have to advertise that the BindingListCollection can do these fun things, so binders can take advantage of it. This is as simple as overriding the various "Supports____" properties of the BindingList and returning true.

That's it! For reals this time!

Oh wait the basil-lime sorbet recipe

Yeah it sucked. Too much simple syrup, not enough lime juice. The basil was good though.

THE END.

Written by Chris

July 17th, 2008 at 9:53 am

One Response to '.NET 3.0 functionality in .NET 2.0: a sorting, filtering, bindable list – Part Two'

Subscribe to comments with RSS or TrackBack to '.NET 3.0 functionality in .NET 2.0: a sorting, filtering, bindable list – Part Two'.

  1. [...] – bookmarked by 3 members originally found by qq12860710 on 2008-07-22 NET 3.0 functionality in .NET 2.0: a sorting, filtering, bindable… http://csammisrun.net/blog/?p=162 – bookmarked by 1 members originally found by toffer on [...]

Leave a Reply