jump to navigation

AggregateCollection March 31, 2007

Posted by Karl Hulme in .Net, C#, WPF, XAML.
trackback

This blog is no longer actively maintained. The content is well over 5 years old – which is like 50 coder years. Use at your own risk!

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
    }
}

Comments»

1. leemon - March 31, 2007

link to source code (aggcollectiontest.zip) is broken

2. Karl Hulme - April 1, 2007

Thanks leemon for letting me know, should be fixed now.

3. Michael Jurka - November 6, 2007

thanks! I was looking for something exactly like this. I’ll have to tweak it a bit to be able to add/remove constituent lists, but it looks like almost all the work is done for me 🙂

4. Michael Jurka - November 10, 2007

DetermineIndexInAggreggateCollection has some incorrect logic. It’s a two line fix, look for the commented lines:

private int DetermineIndexInAggregateCollection(IEnumerable list, int index)
{
if (list == null) throw new ArgumentNullException(“list”);
if (index == -1) return -1;

Int32 adjustedIndex = 0;
for (int i = 0; i < lists.Count; i++)
{
IEnumerable l = lists[i];
if (l == list) { break; }
//else adjustedIndex++;

foreach (Object o in l)
adjustedIndex++;
}

return adjustedIndex + index;
//return list == lists[0] ? adjustedIndex + index + 1 :
// adjustedIndex + index;
}

5. Michael Jurka - November 10, 2007

Whoops, I meant three lines, adjustedIndex must be initialized to 0 as well 🙂

6. Idan - May 26, 2011

thanks! worked great for me.
though there was a bug in the DetermineIndexInAggregateCollection function, when the AggregateCollection receives empty lists.

here my fixed code for that function:

private int DetermineIndexInAggregateCollection(IEnumerable list, int index)
{
if (list == null) throw new ArgumentNullException(“list”);
if (index < 0) return -1;

var adjustedIndex = 0;
foreach (var l in _lists)
{
if (l == list)
{
break;
}

var listCount = l.Cast().Count();

if (listCount > 0 )
{
adjustedIndex += l.Cast().Count();
}
}

return list == _lists[0] ? index : adjustedIndex + index;
}


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: