Tuesday, March 29, 2011

ObservableCollection Filtering

ObservableCollection missed out features
In silverlight projects, we used to work with ObservableCollection for well known reasons. While working so, we can also notice some of the functionalities like AddRange, filtering... missed out, which is required for us at some situations. Similar case arose in my project where I need to provide an observable collection with filtering ability.

Alternate for ObservableCollection filtering
Since we don't have filter option in ObservableCollection, I analysed the effort needed for writing a class which has the basic behaviours of ObservableCollection along with filtering capability. Marc Ridey in his blog have created a FilteredObservableCollection for WPF which I adopted to create one.

Now concentrating on the expected features, we need an object that,
  • holds collection of generic items (T)
  • is able to notify when needed (added, removed, property change notification from the items...)
  • is able to bind with ItemsControl
  • is able to filter.

I decided to just expose the required features. The class should be able to add and remove item(T) even if it doesn't comply with the filtering criteria. But upon iterating, it should expose only the filtered items. If our object is able to notify on collection changed and able to enumerate with filtering ability, then we're 75% nearer to the ObservableCollection with filtering ability.

Find the code below for FilterableNotifiableCollection, that gives the basic features of maintaining a collection and exposes only the filtered items upon iterating it.

Snippet 1
public class FilterableNotifiableCollection<T> : INotifyCollectionChanged, INotifyPropertyChanged, IEnumerable<T>, IEnumerable
{
    #region Class Level Variables

    private ObservableCollection<T> sourceCollection;
    private Predicate<T> currentFilter;

    #endregion

    #region Events

    private event NotifyCollectionChangedEventHandler collectionChanged;
    private event PropertyChangedEventHandler propertyChanged;

    public event NotifyCollectionChangedEventHandler CollectionChanged
    {
        add { collectionChanged += value; }
        remove { collectionChanged -= value; }
    }
    public event PropertyChangedEventHandler PropertyChanged
    {
        add { propertyChanged += value; }
        remove { propertyChanged -= value; }
    }

    #endregion

    #region Properties

    public Predicate<T> Filter
    {
        get { return currentFilter; }
        set
        {
            currentFilter = value;
            this.RefreshCollection();
        }
    }

    public ObservableCollection<T> CoreCollection
    {
        get
        {
            return this.sourceCollection;
        }
    }

    public T this[int index]
    {
        get
        {
            if (this.currentFilter == null)
            {
                return this.sourceCollection[index];
            }
            else
            {
                int currentIndex = 0;
                for (int i = 0; i < this.sourceCollection.Count; i++)
                {
                    T indexitem = this.sourceCollection[i];
                    if (this.currentFilter(indexitem))
                    {
                        if (currentIndex.Equals(index))
                        {
                            return indexitem;
                        }
                        else
                        {
                            currentIndex++;
                        }
                    }
                }
                throw new ArgumentOutOfRangeException();
            }
        }
        set
        {
            if (this.currentFilter == null)
            {
                this.sourceCollection[index] = value;
            }
            else if (!this.currentFilter(value))
            {
                throw new InvalidOperationException("Value doesn't match the filter criteria.");
            }
            else
            {
                bool valueNotSet = true;
                int currentIndex = 0;
                for (int i = 0; i < this.sourceCollection.Count; i++)
                {
                    T indexitem = this.sourceCollection[i];
                    if (this.currentFilter(indexitem))
                    {
                        if (currentIndex.Equals(index))
                        {
                            this.sourceCollection[i] = value;
                            valueNotSet = false;
                            break;
                        }
                        else
                        {
                            currentIndex++;
                        }
                    }
                }

                if (valueNotSet)
                {
                    throw new ArgumentOutOfRangeException();
                }

            }
        }
    }

    #endregion

    #region Constructors

    public FilterableNotifiableCollection(ObservableCollection<T> collectionParam)
    {
        this.sourceCollection = collectionParam;
        currentFilter = null;
        this.sourceCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(SourceCollection_CollectionChanged);
        ((INotifyPropertyChanged)sourceCollection).PropertyChanged += new PropertyChangedEventHandler(SourceCollection_PropertyChanged);
    }

    #endregion

    #region Event Handlers

    void SourceCollection_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (propertyChanged != null)
        {
            propertyChanged(this, e);
        }
    }

    void SourceCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (collectionChanged != null)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    collectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                    break;
                case NotifyCollectionChangedAction.Remove:
                    collectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                    break;
                case NotifyCollectionChangedAction.Replace:
                    collectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                    break;
                case NotifyCollectionChangedAction.Reset:
                    collectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                    break;
                default:
                    break;
            }
        }

    }

    #endregion

    #region Public Methods

    public void Add(T item)
    {
        this.sourceCollection.Add(item);
    }

    public void Remove(T item)
    {
        this.sourceCollection.Remove(item);
    }

    public int IndexOf(T item)
    {
        if (currentFilter == null)
            return this.sourceCollection.IndexOf(item);
        else
        {
            int count = 0;
            for (int i = 0; i < this.sourceCollection.Count; i++)
            {
                T indexitem = this.sourceCollection[i];
                if (currentFilter(indexitem))
                {
                    if (indexitem.Equals(item))
                        return count;
                    else
                        count++;
                }
            }
            return -1;
        }
    }

    public void Clear()
    {
        if (currentFilter == null)
        {
            this.sourceCollection.Clear();
        }
        else
        {
            for (int i = 0; i < this.sourceCollection.Count; )
            {
                T item = this.sourceCollection[i];

                if (currentFilter(item))
                {
                    this.sourceCollection.RemoveAt(i);
                }
                else
                {
                    i++;
                }
            }
        }

    }

    public bool Contains(T item)
    {
        if (currentFilter != null && currentFilter(item) == false)
        {
            return false;
        }

        return this.sourceCollection.Contains(item);
    }

    public int Count
    {
        get
        {
            if (currentFilter == null)
            {
                return this.sourceCollection.Count;
            }
            else
            {
                int count = 0;
                for (int i = 0; i < this.sourceCollection.Count; i++)
                {
                    T item = this.sourceCollection[i];

                    if (currentFilter(item))
                    {
                        count++;
                    }
                }

                return count;
            }
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new FilterableEnumerator(this, this.sourceCollection.GetEnumerator());
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new FilterableEnumerator(this, this.sourceCollection.GetEnumerator());
    }

    public IEnumerable<T> GetFilteredEnumerableGeneric()
    {
        return new FilterableEnumerable(this, this.sourceCollection.GetEnumerator());
    }

    public IEnumerable GetFilteredEnumerable()
    {
        return new FilterableEnumerable(this, this.sourceCollection.GetEnumerator());
    }

    #endregion

    #region Private Methods

    private void RefreshCollection()
    {
        if (collectionChanged != null)
        {
            collectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
    }

    #endregion

    #region Inner Classes

    private class FilterableEnumerable : IEnumerable<T>, IEnumerable
    {
        private FilterableNotifiableCollection<T> filterableNotifiableCollection;
        private IEnumerator<T> enumerator;

        public FilterableEnumerable(FilterableNotifiableCollection<T> filterablecollection, IEnumerator<T> enumeratorParam)
        {
            filterableNotifiableCollection = filterablecollection;
            enumerator = enumeratorParam;
        }

        public IEnumerator<T> GetEnumerator()
        {
            return new FilterableEnumerator(filterableNotifiableCollection, enumerator);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return new FilterableEnumerator(filterableNotifiableCollection, enumerator);
        }
    }

    private class FilterableEnumerator : IEnumerator<T>, IEnumerator
    {
        private FilterableNotifiableCollection<T> filterableObservableCollection;
        private IEnumerator<T> enumerator;

        public FilterableEnumerator(FilterableNotifiableCollection<T> filterablecollection, IEnumerator<T> enumeratorParam)
        {
            filterableObservableCollection = filterablecollection;
            enumerator = enumeratorParam;
        }

        public T Current
        {
            get
            {
                if (filterableObservableCollection.Filter == null)
                    return enumerator.Current;
                else if (filterableObservableCollection.Filter(enumerator.Current) == false)
                    throw new InvalidOperationException();
                else
                    return enumerator.Current;
            }
        }

        public void Dispose()
        {
            enumerator.Dispose();
        }

        object IEnumerator.Current
        {
            get { return this.Current; }
        }

        public bool MoveNext()
        {
            while (true)
            {
                if (enumerator.MoveNext() == false)
                    return false;
                if (filterableObservableCollection.Filter == null
                    || filterableObservableCollection.Filter(enumerator.Current) == true)
                    return true;
            }
        }

        public void Reset()
        {
            enumerator.Reset();
        }
    }

    #endregion

}

The main part I struggled to finalize is the NotifyCollectionChangedAction for which I've used Reset for all the actions. In reference to the msdn article, I took advantage of the following sentence.  You should report Reset in cases where the number of individual Add / Remove / Replace actions necessary to properly report changes in a collection becomes excessive. For example, you should report Reset if a list was completely re-ordered based on some operation such as sorting.
 
This can be further tuned by implementing IList. But in order to make the class simple and just wanted to provide the functionality that we're going to use, I restricted with this. Also I've exposed a property called CoreCollection which returns the original collection, so that if we need the whole collection without filtering, we can make use of that.

Apply Filter
For implementing filter over the FilterableNotifiableCollection, specify a predicate for the Filter property.
 
Snippet 2
private void CheckFilterableNotifiableCollection()
{
    FilterableNotifiableCollection<ContactInfo> contactListTest = new FilterableNotifiableCollection<ContactInfo>(new ObservableCollection<ContactInfo>
    {
        new ContactInfo {ID=111, Email="Balaji@Test.com", Phone="206-555-0108"},
        new ContactInfo {ID=112, Email="Jebarson@Test.com", Phone="206-555-0298"},
        new ContactInfo {ID=113, Email="Jegan@Test.com", Phone="206-555-1130"},
        new ContactInfo {ID=114, Email="RamaPrasad@Test.com", Phone="206-555-0521"}
    }
    );

    contactListTest.Filter = new Predicate<ContactInfo>(FilterByIdGreateThan112);

    listBox.ItemsSource = contactListTest;

}

public bool FilterByIdGreateThan112(ContactInfo contactInfoParam)
{
    if (contactInfoParam.ID > 112)
    {
        return true;
    }
    else
    {
        return false;
    }
}
 
References
http://msdn.microsoft.com/en-us/library/system.collections.specialized.notifycollectionchangedaction(v=vs.95).aspx
http://blog.mridey.com/2009/05/filtered-observablecollection.html

2 comments:

  1. Great stuff, just what I needed and easier to use and understand than LINQ (for me anyhow).

    Thanks for posting this, worked perfectly first time.

    Duncan

    ReplyDelete
  2. Really useful, thanks!

    ReplyDelete

Creative Commons License
This work by Tito is licensed under a Creative Commons Attribution 3.0 Unported License.