Rather
than notifying subscribers of each update, group them into a
meta-event. For a starting example, assume you have a collection that
notifies listeners when it has been updated: class ItemAddedEventArgs<T> : EventArgs
{
private T _item;
public T Item {get;}
public ItemAddedEventArgs(T item)
{
_items = item;
}
}
class MyCollection<T>
{
List<T> _data = new List<T>();
public event EventHandler<ItemAddedEventArgs<T>> ItemsAdded;
protected void OnItemsAdded(T item)
{
if (ItemsAdded != null)
{
ItemsAdded(this, new ItemAddedEventArgs<T>(item));
}
}
public void Add(T item)
{
_data.Add(item);
OnItemsAdded(item);
}
}
The client of this collection happens to be a Windows Form. Here is part of its source code:
public partial class Form1 : Form
{
MyCollection<int> _items = new MyCollection<int>();
public Form1()
{
InitializeComponent();
}
void _items_ItemsAdded(object sender, ItemAddedEventArgs<int> e)
{
listViewOutput.Items.Add(e.Item.ToString());
}
private void buttonOneAtATime_Click(object sender, EventArgs e)
{
_items = new MyCollection<int>();
_items.ItemsAdded += new EventHandler<ItemAddedEventArgs<int>>(_items_ItemsAdded);
GenerateItems();
}
private void GenerateItems()
{
listViewOutput.Items.Clear();
for (int i = 0; i < 20000; i++)
{
_items.Add(i);
}
}
}
When
the button is clicked, 20,000 items are added to the collection, which
generates 20,000 event notifications, and 20,000 inserts and updates
into the ListView.
The ListView has the ability to prevent UI updating during large inserts with the BeginUpdate and EndUpdate methods. This idea could also be used in your custom collection to batch updates.
The ItemAddedEventArgs<T> class must be updated to contain more than one item:
class ItemAddedEventArgs<T> : EventArgs
{
private IList<T> _items = new List<T>();
public IList<T> Items { get { return _items; } }
public ItemAddedEventArgs()
{
}
public ItemAddedEventArgs(T item)
{
_items.Add(item);
}
public void Add(T item)
{
_items.Add(item);
}
}
Here’s the updated MyCollection<T>:
class MyCollection<T>
{
List<T> _data = new List<T>();
int _updateCount = 0;
public event EventHandler<ItemAddedEventArgs<T>> ItemsAdded;
List<T> _updatedItems = new List<T>();
protected void OnItemsAdded(T item)
{
if (!IsUpdating)
{
if (ItemsAdded != null)
{
ItemsAdded(this, new ItemAddedEventArgs<T>(item));
}
}
else
{
_updatedItems.Add(item);
}
}
protected void FireQueuedEvents()
{
if (!IsUpdating && _updatedItems.Count > 0)
{
//the event args have the ability to contain multiple items
ItemAddedEventArgs<T> args = new ItemAddedEventArgs<T>();
foreach (T item in _updatedItems)
{
args.Add(item);
}
_updatedItems.Clear();
if (ItemsAdded != null)
{
ItemsAdded(this, args);
}
}
}
public bool IsUpdating
{
get
{
return _updateCount > 0;
}
}
public void BeginUpdate()
{
//keep a count in case multiple clients call BeginUpdate,
//or it's called recursively, though note that this
//class is NOT thread safe.
++_updateCount;
}
public void EndUpdate()
{
--_updateCount;
if (_updateCount == 0)
{
//only fire when we're done with all updates
FireQueuedEvents();
}
}
public void Add(T item)
{
_data.Add(item);
OnItemsAdded(item);
}
}
Now the client must call BeginUpdate before adding items to take advantage of this:
//in Form class
private void buttonUpdateBatch_Click(object sender, EventArgs e)
{
_items = new MyCollection<int>();
_items.ItemsAdded +=
new EventHandler<ItemAddedEventArgs<int>>(_items_ItemsAdded);
_items.BeginUpdate();
GenerateItems();
_items.EndUpdate();
}
void _items_ItemsAdded(object sender, ItemAddedEventArgs<int> e)
{
listViewOutput.BeginUpdate();
foreach (var i in e.Items)
{
listViewOutput.Items.Add(i.ToString());
}
listViewOutput.EndUpdate();
}
The
time savings can be immense. Refer to the BatchEvents sample program in
the included source code to see the difference. On my machine, the
non-batched version took nearly 6 seconds, whereas the batched version
took less than a second.