Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

EventedList

0.00/5 (No votes)
6 Feb 2006 1  
A generic IList implementation that fires events when items are added or removed.

Introduction

I've written a number of classes that expose collections to which I need to react when items are added or removed. I could obviously write custom code for each of these situations, but that's ugly and I'm lazy.

This article will show you how to create a reusable generic list to which you can react to changes. I will show you how to create an EventedList<T>. Because this class implements IList<T>, it can be used in any code that supports IList<T>, ICollection<T>, IEnumerable<T>, IList and IEnumerable (note that ICollection is not supported, as Microsoft in their infinite wisdom decided not to have ICollection<T> implement ICollection - nonetheless, this could easily be added if you need that functionality).

Using the code

The zip has a number of files. This is because I have a fairly large base library from which it is not trivial to extract components, for example. Sorry about that :( In the zip, I tried to collapse my directory hierarchy, but the files and namespaces remain as they are in my code.

IEvented<T>

I use EventedList<T> in a bunch of my code, but have also found the need for EventedDictionary<T> and EventedSet<T>. In certain situations, as a caller, I'd like to treat the change notification of these classes the same as EventedList<T>. To do this, I first construct the interface IEvented<T>. EventedList<T>, EventedDictionary<T>, and EventedSet<T> will all implement IEvented<T>. If there is demand, and after I post a few more articles, I'll post code for EventedDictionary<T> and EventedSet<T> later.

using System;
using System.Collections.Generic;

namespace RevolutionaryStuff.JBT.Collections
{
    /// <summary>

    /// This interface describes the changes

    /// that are made to an underlying collection

    /// </summary>

    /// <typeparam name="T">The datatype

    /// that is being added/removed</typeparam>

    public interface IEvented<T>
    {
        /// <summary>

        /// Fires when items are added. Iterate through

        /// the enumerable to see exactly what was added

        /// </summary>

        event EventHandler<EventArgs<IEnumerable<T>>> Added;
        /// <summary>

        /// Fires when items are remvoed. Iterate through

        /// the enumerable to see exactly what was removed

        /// </summary>

        event EventHandler<EventArgs<IEnumerable<T>>> Removed;
        /// <summary>

        /// Fired when items are either added or removed

        /// </summary>

        event EventHandler Changed;
    }
}

EventArgs<T>

If you have looked closely at IEvented<T>, you should have seen EventArgs<T>. If you've played with generics much, you would note that the BCL defines EventHandler<T>. So, one would naturally assume that it also defines EventArgs<T>. This is not the case!

    /// <summary>;

    /// This is a simple helper

    /// template so that one can cheat an create 

    /// event handlers w/o explicitly defining new classes.

    /// </summary>

    /// <remarks>

    /// Hey, the framework defines

    /// EventHandler|T|, so why not this?

    /// Probably because it allows you

    /// to cheat too easily and makes

    /// it so you have less flexibility if you

    /// need to add extra parameters to the event

    /// down the road. Nothing wrong with

    /// cheating, but don't use this throughout

    /// </remarks>

    /// <typeparam name="T">The type relevant

    ///            to the event</typeparam>

    public class EventArgs<T> : EventArgs
    {
        /// <summary>

        /// The event's data

        /// </summary>

        public readonly T Data;

        [DebuggerStepThrough]
        public EventArgs(T data)
        {
            this.Data = data;
        }
    }

I assume it is because if you had a classic EventArgs:

    public class FooVersion1EventArgs : EventArgs
    { 
        public bool A;
    }

and needed to upgrade it to:

    public class FooVersion2EventArgs : EventArgs
    {
        public bool A;
        public string B;
        public void Bar();
    }

all of the callers would need to be upgraded. So, pay attention and don't overuse EventArgs<T>. That said, here is the equally useful... and dangerous CancelEventArgs.

    public class CancelEventArgs<T> : System.ComponentModel.CancelEventArgs
    { 
        public readonly T Data;

        [DebuggerStepThrough]
        public CancelEventArgs(T data)
        {
            this.Data = data;
        }    
    }

EventedList<T>

Finally, we can get to the guts of this article. Since the source is included, I've only placed a snippet of this class below.

When you look at this, you'll see that I added support for IsReadOnly which is inexplicably missing from List<T>.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;

namespace RevolutionaryStuff.JBT.Collections
{
    /// <summary>

    /// A list that supports IEvented so the outside world

    /// can tell when changes occur

    /// </summary>

    /// <typeparam name="T">The type of element

    /// being stored in the list</typeparam>

    public class EventedList<T> : IList<T>, IEvented<T>
    {
        /// <summary>

        /// The underlying storage

        /// </summary>

        protected readonly List<T> Inner;

        #region Constructors

        /// <summary>

        /// Construct me

        /// </summary>

        public EventedList()
            : this(null)
        { }

        /// <summary>

        /// Construct me

        /// </summary>

        /// <param name="initialData">The initialization data</param>

        private EventedList(IEnumerable<T> initialData)
        {
            if (initialData == null)
            {
                this.Inner = new List<T>();
            }
            else
            {
                this.Inner = new List<T>(initialData);            
            }
        }

        #endregion

        .
        .
        .
        
        #region IEvented Helpers

        private void OnAdded(params T[] added)
        {
            OnAdded((IEnumerable<T>)added);
        }

        protected virtual void OnAdded(IEnumerable<T> added)
        {
            if (null != this.Added)
            {
                Added(this, new EventArgs<IEnumerable<T>>(added));
            }
            OnChanged();
        }

        private void OnRemoved(T removed)
        {
            OnRemoved(new T[] { removed });
        }

        protected virtual void OnRemoved(IList<T> removed)
        {
            if (null != this.Removed)
            {
                T[] data = new T[removed.Count];
                removed.CopyTo(data, 0);
                Removed(this, new EventArgs<IEnumerable<T>>(removed));
            }
            OnChanged();
        }

        protected virtual void OnChanged()
        {
            DelegateStuff.InvokeEvent(Changed, this);        
        }

        #endregion

        #region IEvented<T> Members

        public event EventHandler<EventArgs<IEnumerable<T>>> Added;
        public event EventHandler<EventArgs<IEnumerable<T>>> Removed;
        public event EventHandler Changed;

        #endregion
    }
}

The only gotcha here is the proper way to handle exceptions. If multiple people subscribe to say... Changed, and the first event handler throws an exception, what should you do?

  • Have no try-catch statements - in which case the person who called Add would be screwed, but at least he'd know of a problem.
  • Catch and suppress the exception - in which case event handlers further down will not be called, but the caller would not be screwed, but you may miss the fact that folks are throwing exceptions.

I opted for the latter. In future, I may add code to manually call each subscriber to isolate each one from the exceptions thrown by the other. But that's for another day.

Example Usage

Below (and of course in the zip) is a dumb example of how to use this:

using System;
using System.Collections.Generic;
using System.Text;
using RevolutionaryStuff.JBT;
using RevolutionaryStuff.JBT.Collections;

namespace EventedListExample
{
    class Program
    {
        static void Main(string[] args)
        {
            IList<int> normalList = new List<int>();
            EventedList<int> eventedList = new EventedList<int>();
            eventedList.Added += eventedList_Added;
            eventedList.Removed += eventedList_Removed;
            eventedList.Changed += eventedList_Changed;

            Test(normalList);
            Test(eventedList);
        }

        private static void Test(ICollection<int> col)
        {
            Console.WriteLine("Testing {0}" + 
              " vvvvvvvvvvvvvvvvvv", col.GetType());
            col.Add(1);
            col.Add(2);
            col.Add(3);
            col.Remove(2);
            col.Add(4);
            StringBuilder sb = new StringBuilder();
            foreach (int i in col)
            {
                sb.AppendFormat("{0}, ", i);
            }
            Console.WriteLine("Items in collection = [{0}]", sb);
            Console.WriteLine("Testing {0}" + 
              " ^^^^^^^^^^^^^^^^^^", col.GetType());
        }

        static void eventedList_Changed(object sender, EventArgs e)
        {
            Console.WriteLine("{0} was changed", sender.GetType());
        }

        static void eventedList_Removed(object sender, 
               EventArgs<IEnumerable<int>> e)
        {
            foreach (int z in e.Data)
            {
                Console.WriteLine("{0} removed {1}", 
                               sender.GetType(), z);
            }
        }

        static void eventedList_Added(object sender, 
               EventArgs<IEnumerable<int>> e)
        {
            foreach (int z in e.Data)
            {
                Console.WriteLine("{0} added {1}", 
                             sender.GetType(), z);
            }
        }
    }
}

This is only my second code submission. If you think you need more explanation or other changes to my format, please speak up so you can influence my future articles.

Happy coding :)

History

  • 2/2/2006 - First submission.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here