Fun with Generics: BindingList and ReadOnlyCollection
Mitch Denny's post about List events and Bill McCarthy's subsequent answer pointing him to BindingList had me looking at some Generics Design Guidelines and there were two generic collection types that I really glad I found out about.
Mitch's question ("why doesn't List<T> fire events") was similar to the one I was asking myself the other day. I was getting tired of providing Add and Remove methods on every object that managed a collection. For example, if I have an Order object and an OrderItem object, I can create Order.AddOrderItem, Order.RemoveOrderItem, but I'd really rather just expose a list of OrderItems from Order, validate each OrderItem as it is added, and allow the standard IList.Remove method to handle removals. It might not be something you'd want to do all the time, but it would mean less code and it could be a useful option to utilise in certain situations. BindingList<T> provides this useful 'list with events' capability.
The other generic collection type I was interested by was ReadOnlyCollection<T>. Have you ever used an IList (probably an ArrayList) internally to manage a list and provided a view of that by converting it to an array? Or maybe you've exposed an array externally? ReadOnlyCollection means that you have an option other than an array with the added benefit of being truly read-only i.e. it doesn't allow something like list[1] = 5;
Here are some tests I used to learn about BindingList<T>and ReadOnlyCollection<T> behaviour.
BindingListTests.cs…
using System;
using System.Collections;
using System.ComponentModel.Collections.Generic;
using NUnit.Framework;
namespace GenericsExamples
{
[TestFixture]
public class BindingListTests
{
bool _EventFired;
[Test]
public void TestBindingListEvents()
{
//Add to a bound list and fail if events are not triggered or triggered incorrectly.
BindingList<int> bindingList = new BindingList<int>();
bindingList.Add(1);
bindingList.ListChanged += new System.ComponentModel.ListChangedEventHandler(bindingList_ListChangedItemAdded);
_EventFired = false;
bindingList.Add(2);
if (!_EventFired) Assert.Fail("BindingList event not triggered");
bindingList.ListChanged -= new System.ComponentModel.ListChangedEventHandler(bindingList_ListChangedItemAdded);
bindingList.ListChanged += new System.ComponentModel.ListChangedEventHandler(bindingList_ListChangedItemDeleted);
_EventFired = false;
bindingList.Remove(2);
if (!_EventFired) Assert.Fail("BindingList event not triggered");
bindingList.ListChanged -= new System.ComponentModel.ListChangedEventHandler(bindingList_ListChangedItemDeleted);
Assert.AreEqual(1, bindingList.Count);
bindingList.ListChanged += new System.ComponentModel.ListChangedEventHandler(bindingList_ListChangedItemAddedButRejected);
bindingList.Add(5);
bindingList.ListChanged -= new System.ComponentModel.ListChangedEventHandler(bindingList_ListChangedItemAddedButRejected);
Assert.AreEqual(1, bindingList.Count);
}
void bindingList_ListChangedItemAdded(object sender, System.ComponentModel.ListChangedEventArgs e)
{
if (e.ListChangedType != System.ComponentModel.ListChangedType.ItemAdded)
{
Assert.Fail("Expected ItemAdded event but got: " + e.ListChangedType.ToString());
}
_EventFired = true;
}
void bindingList_ListChangedItemDeleted(object sender, System.ComponentModel.ListChangedEventArgs e)
{
if (e.ListChangedType != System.ComponentModel.ListChangedType.ItemDeleted)
{
Assert.Fail("Expected ItemDeleted event but got: " + e.ListChangedType.ToString());
}
_EventFired = true;
}
void bindingList_ListChangedItemAddedButRejected(object sender, System.ComponentModel.ListChangedEventArgs e)
{
if (e.ListChangedType == System.ComponentModel.ListChangedType.ItemAdded)
{
//Do some validation and if it fails then I might need to remove the added item
((IList)sender).RemoveAt(e.NewIndex);
}
}
}
}
ReadOnlyCollectionTests.cs…
using System;
using System.Collections.Generic;
using NUnit.Framework;
namespace GenericsExamples
{
[TestFixture]
public class ReadOnlyCollectionTests
{
[Test]
public void TestReadOnlyCollectionCreation()
{
List<int> list = new List<int>();
list.Add(1);
list.Add(2);
System.Collections.Generic.ReadOnlyCollection<int> roList = new ReadOnlyCollection<int>(list);
//roList.Add(3); //Operation isn't available!
//roList[1] = 3; //Operation isn't available!
Assert.AreEqual(2, list.Count);
Assert.AreEqual(2, roList.Count);
}
}
}
I wonder if they'll improve BindingList<T> before RTM by getting ListChangedEventHandler to pull the generic type from the BindingList rather than simply providing a parameter of "object sender"? We'll just have to wait and see.
For more about handling different situations with Generics, have a read of the Generics Design Guidelines from Krzysztof Cwalina.