AggregateCollection March 31, 2007
Posted by Karl Hulme in .Net, C#, WPF, XAML.trackback
My apparent obsession with collection classes continues, and hopefully reaches it’s conclusion, with the AggregateCollection class. Simply put, it allows you to access multiple collections from a single collection object. A basic usage would be something like…
private void TestAggregate()
{
List<Int32> listOne = new List<Int32>();
List<String> listTwo = new List<String>();
for (int i = 0; i < 10; i += 2)
{
listOne.Add(i + 1);
listTwo.Add(i.ToString());
}
AggregateCollection agg = new AggregateCollection(listOne, listTwo);
foreach (Object o in agg)
{
Console.WriteLine(o.ToString());
}
}
And the output is as you might expect…
The lists passed to the constructor need only support IEnumerable, but if they also support INotifyCollectionChanged then you’ll get a better WPF binding experience. I wrote this class for WPF in the first place, because I want to be able to pull information together from different lists into the UI and I don’t want to be monitoring for the change events myself.
The AggregateCollection class itself supports the IEnumerable interface of course, and it also implements IList (which thus brings ICollection into the fray). The fact that it implements IList, means that even if the constituent lists don’t support IList, you can still access all the objects in the aggregate collection via a single index (from 0 to “count of all the lists”). I think this is quite cool, but it serves a greater purpose…
When you bind to a collection class in WPF, a CollectionView (or derivative) is created as a wrapper around your collection, and WPF will bind to that instead. This allows items to be sorted, grouped, etc, without affecting the underlying data. Bea Costa (data binding supremo) explains this very well (post November 22nd ‘06). The AggregateCollection class supports IList, so that at binding-time WPF will instantiate a ListCollectionView, instead of a CollectionView - which is necessary to support sorting.
The example app below demonstrates the AggregateCollection class in action in WPF, and shows how, with sorting, this can be helpful. The first and second columns show the contents of two seperate ObservableCollections. There’s also some buttons for adding and removing objects from those collections, and for moving those items around. The ListBox in the third column is bound to an AggregateCollection containing the first 2 lists. The ListBox in the fourth column is bound to a sorted CollectionViewSource which references the same AggregateCollection. As the items in the first two collections change the ListBoxes in columns 3 and 4 will update automatically.
Example application exe (save to your local machine first, then run)
Example application solution
The AggregateCollection class can’t be synchronised, because I only intended for it to be used in the UI layer, but I guess there would be a way to make this work. I also made it read-only because I couldn’t determine a satisfactory strategy for which constituent list a new item should be added to. Finally, you can’t add or remove constituent lists after it’s been created, but there’s no fundamental obstacle to implementing this. The code for the AggregateCollection class is below.
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows.Threading;
using System.Collections;
using System.Collections.ObjectModel;
namespace AggCollectionTest
{
public class AggregateCollection : IEnumerable, IList, INotifyCollectionChanged
{
private IEnumerable[] lists;
private List<Object> cache = new List<Object>();
private Boolean cacheValid = false;
private Object syncRoot = new Object();
public AggregateCollection(params IEnumerable[] lists)
{
if (lists == null) throw new ArgumentNullException("lists");
this.lists = lists;
foreach (IEnumerable list in lists)
{
if (list is INotifyCollectionChanged)
{
((INotifyCollectionChanged)list).CollectionChanged +=
delegate(Object sender, NotifyCollectionChangedEventArgs e)
{
cacheValid = false;
NotifyCollectionChangedEventArgs eventArgs;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
eventArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add,
e.NewItems, DetermineIndexInAggregateCollection(sender as IEnumerable, e.NewStartingIndex));
break;
case NotifyCollectionChangedAction.Move:
eventArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move,
e.NewItems, DetermineIndexInAggregateCollection(sender as IEnumerable, e.NewStartingIndex),
DetermineIndexInAggregateCollection(sender as IEnumerable, e.OldStartingIndex));
break;
case NotifyCollectionChangedAction.Remove:
eventArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove,
e.OldItems, DetermineIndexInAggregateCollection(sender as IEnumerable, e.OldStartingIndex));
break;
case NotifyCollectionChangedAction.Replace:
eventArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace,
e.NewItems, e.OldItems, DetermineIndexInAggregateCollection(sender as IEnumerable, e.OldStartingIndex));
break;
default:
case NotifyCollectionChangedAction.Reset:
eventArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
break;
}
OnCollectionChanged(eventArgs);
};
}
}
}
private int DetermineIndexInAggregateCollection(IEnumerable list, int index)
{
if(list == null) throw new ArgumentNullException("list");
if (index == -1) return -1;
Int32 adjustedIndex = -1;
for (int i = 0; i < lists.Length; i++)
{
IEnumerable l = lists[i];
if (l == list) { break; }
else adjustedIndex++;
foreach (Object o in l)
adjustedIndex++;
}
return list == lists[0] ? adjustedIndex + index + 1 :
adjustedIndex + index;
}
#region IEnumerable Members
public IEnumerator GetEnumerator()
{
return new Enumerator(lists);
}
private class Enumerator : IEnumerator
{
private Int32 listIndex = -1;
private IEnumerator[] enumerators;
public Enumerator(IEnumerable[] lists)
{
enumerators = new IEnumerator[lists.Length];
for (int i = 0; i < lists.Length; i++)
enumerators[i] = lists[i].GetEnumerator();
}
public object Current
{
get
{
try
{
return enumerators[listIndex].Current;
}
catch (IndexOutOfRangeException)
{
throw new InvalidOperationException();
}
}
}
public bool MoveNext()
{
if (listIndex > -1 && listIndex < enumerators.Length &&
enumerators[listIndex].MoveNext())
{
return true;
}
else
{
listIndex++;
if (listIndex < enumerators.Length)
{
try
{
enumerators[listIndex].Reset();
}
catch (InvalidOperationException)
{
return false;
}
return MoveNext();
}
else return false;
}
}
public void Reset()
{
listIndex = -1;
}
}
#endregion
#region INotifyCollectionChanged Members
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e == null) throw new ArgumentNullException("e");
if (CollectionChanged != null)
CollectionChanged(this, e);
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
#endregion
#region IList Members
public int Add(object value)
{
throw new NotSupportedException();
}
public void Clear()
{
throw new NotSupportedException();
}
public bool Contains(object value)
{
return IndexOf(value) != -1;
}
public int IndexOf(object value)
{
Int32 i = -1;
foreach (Object o in this)
{
i++;
if ((value == null && o == null) | (value != null && value.Equals(o)))
{
return i;
}
}
return -1;
}
public void Insert(int index, object value)
{
throw new NotSupportedException();
}
public bool IsFixedSize
{
get { return false; }
}
public bool IsReadOnly
{
get { return true; }
}
public void Remove(object value)
{
throw new NotSupportedException();
}
public void RemoveAt(int index)
{
throw new NotSupportedException();
}
public object this[int index]
{
get
{
if (!cacheValid)
{
cache.Clear();
foreach (Object o in this)
{
cache.Add(o);
}
cacheValid = true;
}
return cache[index];
}
set
{
throw new NotSupportedException();
}
}
#endregion
#region ICollection Members
public void CopyTo(Array array, int index)
{
Int32 i = -1;
foreach (Object o in this)
array.SetValue(o, index + ++i);
}
public int Count
{
get
{
Int32 count = 0;
foreach (Object o in this) count++;
return count;
}
}
public bool IsSynchronized
{
get { return false; }
}
public object SyncRoot
{
get { return syncRoot; }
}
#endregion
}
}
link to source code (aggcollectiontest.zip) is broken
Thanks leemon for letting me know, should be fixed now.