Implementing CollectionBase

Matt Berther bio photo By Matt Berther Comment

Update: The majority of what is in this article has been deprecated by the introduction of generic collections in the .NET framework. More often than not, you would just create an instance of List, Dictionary, etc.

Update: For you code-genners out there, I've created a CodeSmith template that I use to generate these implementations. Download it here. Also, Chris Lane has created a VB.NET version of the CodeSmith template, available here. Enjoy!

Update: Leif Wickland has updated the C# template to get rid of three compiler warnings. Make sure to get the new version. Thanks, Leif!

CollectionBase is probably one of my favorite classes in the .NET 1.1 framework. I tend to do a lot of work that involves creating collections of objects and I really enjoy having strongly typed access to my collections.

Implementing CollectionBase is very easy. Usually, you'll just add an Add and Remove method. An indexer, IndexOf and a Contains method, if you like. Simple.

There are a few things to keep in mind when implementing CollectionBase, however.

It is important to override the OnValidate method. This method should throw an Exception if the passed object does not belong in the current collection. I think this is probably the one thing I see neglected most often. The reason why this is important is that CollectionBase has an explicit IList implementation. This means that a user can cast your CollectionBase derived object to an IList and call any of the methods on the IList. Suddenly, your user is putting incorrect objects into your collection.

For example:

public class PersonCollection : CollectionBase
{
    protected override void OnValidate(object value)
    {
        base.OnValidate(value);
        if (!(value is Person))
        {
            throw new ArgumentException("Collection only supports Person objects.");
        }
    }
}

Also important is to mark your new collection with the Serializable attribute. The Serializable attribute is applied to the CollectionBase class, however, this attribute is not inherited. It's easier to do this now rather than later when you find a serialization bug that has manifested itself in some strange way. Trust me, just add the attribute.

A lot of times, you'll want to return a ReadOnly collection from one of your properties. For example, say you're returning a list of States to populate a state list. You will not want to allow the user to modify this collection once it has been generated by your object. You can accomplish this and still use CollectionBase.

[Serializable]
public class PersonCollection : CollectionBase
{
    public PersonCollection()
    {
    }

    public PersonCollection(PersonCollection coll)
    {
        this.InnerList.AddRange(coll);
    }

    public Person this[int index]
    {
        get { return (Person)List[index]; }
        set { List[index] = value; }
    }

    public virtual void Add(Person person)
    {
        List.Add(person);
    }

    public virtual void Remove(Person person)
    {
        List.Remove(person);
    }

    public bool Contains(Person person)
    {
        return List.Contains(person);
    }

    public int IndexOf(Person person)
    {
        return List.IndexOf(person);
    }

    public static PersonCollection ReadOnly(PersonCollection coll)
    {
        return new PersonCollection.ReadOnlyPersonCollection(coll);
    }

    protected override void OnValidate(object value)
    {
        base.OnValidate(value);
        if (!(value is Person))
        {
            throw new ArgumentException("Collection only supports Person objects.");
        }
    }

    #region ReadOnlyPersonCollection
    private sealed class ReadOnlyPersonCollection : PersonCollection
    {
        private const string ERROR_STRING = "Collection is read-only.";

        internal ReadOnlyPersonCollection(PersonCollection coll) : base(coll)
        {
        }

        public override void Add(Person person)
        {
            throw new NotSupportedException(ERROR_STRING);
        }

        public override void Remove(Person person)
        {
            throw new NotSupportedException(ERROR_STRING);
        }

        protected override void OnClear()
        {
            throw new NotSupportedException(ERROR_STRING);
        }

        protected override void OnInsert(int index, object value)
        {
            throw new NotSupportedException(ERROR_STRING);
        }

        protected override void OnRemove(int index, object value)
        {
            throw new NotSupportedException(ERROR_STRING);
        }

        protected override void OnSet(int index, object oldValue, object newValue)
        {
            throw new NotSupportedException(ERROR_STRING);
        }
    }
    #endregion
}

We added a static method called ReadOnly, which takes a PersonCollection and returns a PersonCollection. Inside of our PersonCollection object, we have a nested class called ReadOnlyPersonCollection, which derives from PersonCollection and provides the implementation that will make the collection read-only. Since ReadOnlyPersonCollection derives from PersonCollection, we have no problem returning it from our static ReadOnly method.

We see here that our ReadOnlyPersonCollection is also overriding a few additional methods. CollectionBase.Clear is not marked as virtual, so thankfully, Microsoft has provided us a way to stop the Clear from occurring. Prior to the list.Clear() call, OnClear() is called. Similar functionality is available for insert, update and delete. These methods are all overridden, since a read-only collection should not be able to be modified in any way.

Of course, .NET 2.0, with the introduction of generics, will render all of this information invalid. All of my CollectionBase implementations will be replaced with IList<Type>. :=)