On the project I'm working on at the moment, I have a class that stores items in an ObservableCollection. I needed to be able to expose in the same class different subsets of this collection, keeping them all in sync while allowing to add/remove/update elements in all the collections.
For example, imagine you would like to create a collection of people ObservableCollection<People> but also have collection for Males and Females and pass these collections as parameters to other methods that can manipulate these collections (Add/Remove/Update People in the Males collection) and see the original collection updated.
My first approach was to create an ObservableCollection for each subset and use the CollectionChanged event to maintain all the lists in sync. While it worked, it was very code heavy. So today I wrote a simple FilteredObservableCollection class. It encapsulates an ObservableCollection but only enumerates the items that comply with the filter. It's similar to the CollectionView but it still allows to Add/Remove/Update elements in the collection.
The class is not derived from an ObservableCollection since it needs to be able to attach to an existing collection but it expose all the interfaces and members for the observable collection:
public
class
FilteredObservableCollection<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable, INotifyCollectionChanged, INotifyPropertyChanged
{
private
ObservableCollection<T> _collection;
private
Predicate<T> _filter;
private
event
NotifyCollectionChangedEventHandler _collectionchanged;
private
event
PropertyChangedEventHandler _propertychanged;
public FilteredObservableCollection(ObservableCollection<T> collection)
{
_filter = null;
_collection = collection;
_collection.CollectionChanged += new
NotifyCollectionChangedEventHandler(OnCollectionChanged);
((INotifyPropertyChanged)_collection).PropertyChanged += new
PropertyChangedEventHandler(OnPropertyChanged);
}
The two interesting bit in the Filtered collection are the handling on the CollectionChanged event and the enumerator:
The CollectionChanged event received from the encapsulated collection may not necessarily need to be passed by the filtered collection. An item added to or deleted from the main collection, if filtered out, should not generate an event for the filtered collection while a Change to an existing item may cause the item to appear in the list (it now complies with the filter) or disappear from the list (it no longer complies with the filter).
private
void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_collectionchanged != null)
{
// Check the NewItems
List<T> newlist = new
List<T>();
if (e.NewItems != null)
foreach (T item in e.NewItems)
if (_filter(item) == true)
newlist.Add(item);
// Check the OldItems
List<T> oldlist = new
List<T>();
if (e.OldItems != null)
foreach (T item in e.OldItems)
if (_filter(item) == true)
oldlist.Add(item);
// Create the Add/Remove/Replace lists
List<T> addlist = new
List<T>();
List<T> removelist = new
List<T>();
List<T> replacelist = new
List<T>();
// Fill the Add/Remove/Replace lists
foreach (T item in newlist)
if (oldlist.Contains(item))
replacelist.Add(item);
else
addlist.Add(item);
foreach (T item in oldlist)
if (newlist.Contains(item))
continue;
else
removelist.Add(item);
// Send the corrected event
switch (e.Action)
{
case
NotifyCollectionChangedAction.Add:
case
NotifyCollectionChangedAction.Move:
case
NotifyCollectionChangedAction.Remove:
case
NotifyCollectionChangedAction.Replace:
if (addlist.Count > 0)
_collectionchanged(this, new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, addlist));
if (replacelist.Count > 0)
_collectionchanged(this, new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, replacelist));
if (removelist.Count > 0)
_collectionchanged(this, new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removelist));
break;
case
NotifyCollectionChangedAction.Reset:
_collectionchanged(this, new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
break;
}
}
}
The enumerator starts from the first compliant element in the collection and each call to Next() makes the cursor move to the next valid filtered item.
private
class
FilteredEnumerator : IEnumerator<T>, IEnumerator
{
private
FilteredObservableCollection<T> _filteredcollection;
private
IEnumerator<T> _enumerator;
public FilteredEnumerator(FilteredObservableCollection<T> filteredcollection, IEnumerator<T> enumerator)
{
_filteredcollection = filteredcollection;
_enumerator = enumerator;
}
public T Current
{
get
{
if (_filteredcollection.Filter == null)
return _enumerator.Current;
else
if (_filteredcollection.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 (_filteredcollection.Filter == null
|| _filteredcollection.Filter(_enumerator.Current) == true)
return
true;
}
}
public
void Reset()
{
_enumerator.Reset();
}
}
}
It's worked well so far but a lot remains to be done. I need to optimize the code to avoid for example having to calculate the Count at every call. I need to test all the possible collection manipulation and see if the CollectionChanged event is processed correctly. I need to implement the missing Move() method. I need to test multi-threading.
I've made the code available for download as part of a sample project that displays a list of integers and two subsets of multiples of 2 and of 3.
public
class
Data : DependencyObject
{
public
static
readonly
DependencyProperty FullListProperty =
DependencyProperty.Register("FullList", typeof(ObservableCollection<int>), typeof(Data));
public
static
readonly
DependencyProperty List_2Property =
DependencyProperty.Register("List_2", typeof(FilteredObservableCollection<int>), typeof(Data));
public
static
readonly
DependencyProperty List_3Property =
DependencyProperty.Register("List_3", typeof(FilteredObservableCollection<int>), typeof(Data));
public
ObservableCollection<int> FullList
{
get { return (ObservableCollection<int>)GetValue(FullListProperty); }
set { SetValue(FullListProperty, value); }
}
public
FilteredObservableCollection<int> List_2
{
get { return (FilteredObservableCollection<int>)GetValue(List_2Property); }
set { SetValue(List_2Property, value); }
}
public
FilteredObservableCollection<int> List_3
{
get { return (FilteredObservableCollection<int>)GetValue(List_3Property); }
set { SetValue(List_3Property, value); }
}
public Data()
{
FullList = new
ObservableCollection<int>();
List_2 = new
FilteredObservableCollection<int>(FullList);
List_3 = new
FilteredObservableCollection<int>(FullList);
List_2.Filter = new
Predicate<int>(Filter_2);
List_3.Filter = new
Predicate<int>(Filter_3);
}
private
bool Filter_2(int x) { return x % 2 == 0; }
private
bool Filter_3(int x) { return x % 3 == 0; }
}
I've used a DependencyObject and DependencyProperties but it wasn't necessary to use the FilteredObservableCollection.
Download the project source code here: Download