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
{
public interface IEvented<T>
{
event EventHandler<EventArgs<IEnumerable<T>>> Added;
event EventHandler<EventArgs<IEnumerable<T>>> Removed;
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!
public class EventArgs<T> : EventArgs
{
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
{
public class EventedList<T> : IList<T>, IEvented<T>
{
protected readonly List<T> Inner;
#region Constructors
public EventedList()
: this(null)
{ }
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.