Using CollectionViewSource without Refresh() for Faster Filtering in Silverlight

CollectionViewSource is quickly becoming one of my go-to tools for complex data viewing and editing in Silverlight.  The Grouping, Filtering, and Sorting operations make what were once difficult features simple and straightforward.  But I sometimes expect too much from CollectionViewSource’s performance with large data sets, and then things get more interesting.

I was working with large sets of complex data in a DataGrid, using the CollectionViewSource’s Filter operations to allow the user to ‘Delete’ rows while still keeping the items in the underlying collection to allow either a ‘Cancel’ or a ‘Save’ operation to finalize the change.  Since CollectionViewSource’s Filtering is only checked on Collection changes, such as Adding or Removing an item from the collection itself – changing a Property on an existing item (even if that property would affect the Filtered status), does not result in the item being removed from the View.

So, even if the objects being bound have a Property called State:

public enum DataState
{
    Unchanged,
    Added,
    Deleted
}

public class TestData : INotifyPropertyChanged
    {
        private DataState _state;
        public DataState State
        {
            get
            {
                return _state;
            }
            set
            {
                _state = value;
                DoPropertyChanged("State");
            }
        }

…..

And the State is changed by the user clicking a button:

private void btnDelete_Click(object sender, RoutedEventArgs e)
{
    if (dgMain.SelectedItem != null)
    {
        (dgMain.SelectedItem as TestData).State = DataState.Deleted;
    }
}

The Item will still be visible in the DataGrid until something happens that causes the CollectionViewSource to re-check its filters.  Notice that although Data2 and Data3 have a State of ‘Deleted’, they are still visible in the grid bound to the view:

 

For small data sets, we can always call CollectionViewSource.View.Refresh(), which will force the whole view to be recreated, and check the filtering for each item.  It works, but it is far too slow for lots of data, especially when special formatting is required for the UI.  Why re-check every item, when we know which item should be removed?

Since we know the CollectionViewSource already watches for removed and added events on the source collection, we can use that to create a derived type that lets us send those messages to explicitly notify the view when an item should not be shown (or one should be added), until the view is rebuilt the next time.  In my case, it turned what was once a several-second delay on deleted items into an instant update.

The class is simple, and calling the DoNotifyCollectionChangeRemove or DoNotifyCollectionChangeAdd methods does not actually change the collection at all!  It just tells the view to look for an added or removed item, without requiring it to check ALL of the items for changes.

public class NotifiableObservableCollection<T> : ObservableCollection<T>
{
    #region DoNotifyCollectionChanged
    /// <summary>
    /// Use this method to foce a reset operation on any views bound to this collection
    /// </summary>
    public void DoNotifyCollectionChanged()
    {
        base.OnCollectionChanged(
            new System.Collections.Specialized.NotifyCollectionChangedEventArgs(
                System.Collections.Specialized.NotifyCollectionChangedAction.Reset));
    }
    #endregion

    #region DoNotifyCollectionChangeRemove
    /// <summary>
    /// Use this method to force any views bound to this collection to look for a specific
    /// item as deleted..use after changing any property that would cause the object to be filtered.
    /// </summary>
    /// <param name="obj"></param>
    public void DoNotifyCollectionChangeRemove(T obj, int index)
    {
        base.OnCollectionChanged(
            new System.Collections.Specialized.NotifyCollectionChangedEventArgs(
                System.Collections.Specialized.NotifyCollectionChangedAction.Remove, obj, index));
    }
    #endregion

    #region DoNotifyCollectionChangeAdd
    /// <summary>
    /// Use this method to force any views bound to this collection to look for a specific
    /// item as inserted, use after any operation that should cause new items to appear
    /// </summary>
    /// <param name="obj"></param>
    public void DoNotifyCollectionChangeAdd(T obj, int index)
    {
        base.OnCollectionChanged(
            new System.Collections.Specialized.NotifyCollectionChangedEventArgs(
            System.Collections.Specialized.NotifyCollectionChangedAction.Add, obj, index));
    }
    #endregion
}

 

You still have to handle calling of these methods yourself, by watching property changes on the Items themselves, but in scenarios like our ‘Deleted’ example, it is simple to add:

private void GenerateData()
{
    for (int i = 0; i < numData; i++)
    {
        TestData td = new TestData
        {
            Name = String.Format("{0}{1}", data, i),
            State = DataState.Unchanged,
            Value = i
        };
        td.PropertyChanged += td_PropertyChanged;
        dataList.Add(td);
    }
}
private void td_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "State")
    {
        TestData td = sender as TestData;
        if (td.State == DataState.Deleted)
        {
            int index = dataList.IndexOf(td);
            dataList.DoNotifyCollectionChangeRemove(td, index);
        }
    }
}

Now as soon as the State property changes to Deleted, we force the NotifyCollectionChangedEvent on that item, and it disappears from the DataGrid.

 

If you happen to be using Telerik controls, it is even easier using Telerik’s ObservableItemCollection generic class, since it provides automatic observation of the property values of the Items within the collection (you do not have to assign the PropertyChanged handler yourself).  A similar strategy can also be used to Cancel deletes using DoNotifyCollectionChangeAdd().  In fact, we have found several uses for this class, and maybe you will too!

 

You can download the code for this example HERE.

comments powered by Disqus