Implementing CollectionBase
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.
Take a look at this:
[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>. :=)
Christian, thanks for the tip. You’re completely right. I’ve added the implementation of IndexOf to the sample code.
Implementing CollectionBase the right way
Collection Base Implementation Tips
Well, what happens if you give a ReadOnly copy to a client and you modify the original collection?
PersonCollection collection = new PersonCollection();
collection.Add(new Person());
PersonCollection readOnlyCollection = PersonCollection.ReadOnly(collection);
collection.Add(new SomeClass());
Now collection has two items and readOnlyCollection has only one.
The ReadOnly copy will not be in-sync. What you really want is for the ReadOnly static class to not be a true instance of a collection but rather a simply wrapper that forwards any calls to the actual collection.
PersonCollection innerCollection;
internal ReadOnlyPersonCollection(PersonCollection collection)
{
this.innerCollection = collection;
}
example method:
public override bool Contains(Person person)
{
return this.innerCollection.Contains(person);
}
This works nicely until you realise that CollectionBase implements a Count property which you cannot override. Great.
Angleos: You have a very valid point. Regarding CollectionBase.Count, I may be way off, but wouldnt
public new int Count { get return innerCollection.Count; }
do the trick?
Unfortunately that would not work. The new keyword means that the method/property is available only when the object is casted to the type that the method/property is specified in.
Here is an example with the new Count property implemented.
PersonCollection collection = new PersonCollection();
collection.Add(new Person());
collection.Add(new Person());
ReadOnlyPersonCollection readOnlyCollection = PersonCollection.ReadOnly(collection);
Console.WriteLine(readOnlyCollection.Count.ToString());
PersonCollection castedReadOnlyCollection = (PersonCollection) readOnlyCollection;
Console.WriteLine(castedReadOnlyCollection.Count.ToString())
The output would be:
2
0
I’ll concede the implementation of ReadOnly collection. I’ll also say that I do like the way things are done in Java, regarding inheritance. You can override *anything* unless it is specifically declared final. I’m not sure why this is backwards in C#.
The other points are still valid when implementing CollectionBase…
I haven’t given it great thought, but I feel the same way regarding overriding anything unless explicitly not being allowed to. It is more logical in the sense that it is more likely to identify a method as one an inheriting class should not be able to change, rather than one it will need to change.
I really cannot understand why they created a base (as the name demonstrates) class that has a property that cannot be overridden.
For my custom collection needs I resorted in custom classes that implement all the required interfaces (ICollection, IList, etc.) from the ground up, using arrays internally. It’s such a bad feeling having to copy paste and doing search and replace every time I need a new strongly typed collection …
If you havent looked at it, this is where CodeSmith comes in really handy.
Code generation of things like this has saved me a significant amount of time lately.
Can’t wait till we can just new ReadOnlyCollection<Person>();
As I mentioned in the last paragraph of my article, all of this code will be replaced with one line in .NET 2.0.
IList<Person> coll = new List<Person>();
I love how technology advances… :=)
I am a newbie so I was just wondering,
Why not just use the ReadOnlyCollectionBase Class?
Chris: Ultimately, I believe that using ReadOnlyCollectionBase suffers from the same limitations. Once you modify the original copy, the ReadOnly copy is invalid.
I am having problems trying to convert this example to VB.NET. VS is complaining about the multipile inherits with in a class since the readonly class is within the other class and the regions tags are messed up. Does anybody have a VB.NET version of this template or the result of the template?
Here is what I have so far.
Imports System
Imports System.Collections
Imports Customer
Namespace BusinessObjects.MyClass
_
Public Class CustomerCollection
Inherits CollectionBase
Public Sub New()
End Sub
Public Sub New(ByVal col1 As CustomerCollection)
Me.InnerList.AddRange(col1)
End Sub
Default Public Property aCustomer(ByVal index As Integer) As Customer
Get
Return CType(List(index), Customer)
End Get
Set(ByVal Value As Customer)
List(index) = Value
End Set
End Property
Public Overridable Function Add(ByVal value As Customer) As Integer
Return List.Add(value)
End Function
Public Overridable Sub Insert(ByVal index As Integer, ByVal value As Customer)
List.Insert(index, value)
End Sub
Public Overridable Sub Remove(ByVal value As Customer)
List.Remove(value)
End Sub
Public Function Contains(ByVal value As Customer) As Boolean
Return List.Contains(value)
End Function
Public Function IndexOf(ByVal value As Customer) As Integer
Return List.IndexOf(value)
End Function
Public Sub CopyTo(ByVal value() As Customer, ByVal index As Integer)
List.CopyTo(value, index)
End Sub
Public Static Function ReadOnlyCustomerCollection(ByVal col1 As CustomerCollection)
Return New CustomerCollection.ReadOnlyCustomerCollection(coll)
End Function
Protected Overrides Sub OnValidate(ByVal value As Object)
MyBase.OnValidate(value)
If Not TypeOf value Is Customer Then
Throw New ArgumentException(”Collection only supports Customer objects.”)
End If
End Sub
Public Enum AccessibilityEnum
[Public]
[Protected]
[Internal]
[ProtectedInternal]
[Private]
End Enum
Public Function GetAccessModifier(ByVal accessibility As AccessibilityEnum) As String
Select Case accessibility
Case AccessibilityEnum.Public
Return “public”
Case AccessibilityEnum.Protected
Return “protected”
Case AccessibilityEnum.Internal
Return “internal”
Case AccessibilityEnum.ProtectedInternal
Return “protected internal”
Case AccessibilityEnum.Private
Return “private”
Case Else
Return “public”
End Select
End Function
#Region “ReadOnlyClass”
Private NonInheritable Class ReadOnlyCustomerCollection
Inherits CustomerCollection
Private Const ERROR_STRING As String = “Collection is read-only.”
Friend ReadOnly Sub New(ByVal value As Customer)
MyBase.New(coll)
End Sub
Public Overrides Function Add(ByVal value As Customer) As Integer
Throw New NotSupportedException(ERROR_STRING)
End Function
Public Overrides Sub Remove(ByVal value As Customer)
Throw New NotSupportedException(ERROR_STRING)
End Sub
Protected Overrides Sub OnClear()
Throw New NotSupportedException(ERROR_STRING)
End Sub
Protected Overrides Sub OnInsert(ByVal index As Integer, ByVal value As Object)
Throw New NotSupportedException(ERROR_STRING)
End Sub
Protected Overrides Sub OnRemove(ByVal index As Integer, ByVal value As Object)
Throw New NotSupportedException(ERROR_STRING)
End Sub
Protected Overrides Sub OnSet(ByVal index As Integer, ByVal oldValue As Object, ByVal NewValue As Object)
Throw New NotSupportedException(ERROR_STRING)
End Sub
End Class
#end Region
End Class
End Namespace
Chris,
Im not sure what you’re trying to do here. It looks like you’re trying to combine the codesmith file with a .vb file.
Which one are you trying to get to work?
Matt,
Doh, Sorry about that, The code I posted is an attempt at trying to convert the output of the C# file that the template creates to VB.NET, then I was going to convert the template to VB Style.
Thank You,
Chris
And you’re using CodeSmith to transform the template? If so, send me an email with your file as an attachment, and Ill be glad to take a look at it.
Chris,
I found error in VB.NET version of template. Instead of
Default Public Property aCustomer(ByVal index As Integer) As Customer
it should generate
Default Public Property Item(ByVal index As Integer) As Customer
If this property is not named “Item”, collection editor in PropertyGrid does not recognize generated collection as a strongly-typed collection.
Matej Rizman
Sir
I created collection property using collection base class.I want to limit the object added (say 10) to the collection.If it is greater than 10 i want to throw an error dialogbox.How to control the no. of objects added to a collection in design time.
It seems to me that you would need to implement the Add method to perform a check if the inner list has a count >= 10 before adding.
public void Add(MyType obj)
{
if (InnerList.Count >= 10)
{
throw new ApplicationException(”This collection can only hold 10 objects.”);
}
Innerlist.Add(obj);
}
i want to create windows installer project in which i want to restrict the installation based on the ‘Product serial number’ entered in the ‘Customer information’ user interface custom dialog box. i also added the custom action and able to validate the product serial number entered. but my problem is unable to terminate the installation if user enters the wrong serial number.
can anybody share your experience on this ?
Thanks in advance.
Sudhakar-D
chudhakar@yahoo.com
heres a nice update for this article: a C# code snippet for vs 2005. just place the xml into a .snippet file and add to vs from the tools menus Code Snippet Manager.
once imported type the short name classCollectionBase and press tab from within the ide…
Strongly Typed CollectionBase Class
Eric Maroudas
Creates a Strongly Typed Collection Base Class Implementation.
classCollectionBase
Expansion
ClassName
Replace with your collection base class name.
className
TypeName
Replace with your custom type class.
yourType
ItemName
Replace with an item name of your choice
item
{
#region Implemantation
public int Add($TypeName$ $ItemName$)
{
return ((IList)this).Add((object)$ItemName$);
}
public void Remove($TypeName$ $ItemName$)
{
((IList)this).Remove((object)$ItemName$);
}
public void Insert(int index, $TypeName$ $ItemName$)
{
((IList)this).Insert(index, (object)$ItemName$);
}
public bool Contains($TypeName$ $ItemName$)
{
return ((IList)this).Contains((object)$ItemName$);
}
public int IndexOf($TypeName$ $ItemName$)
{
return ((IList)this).IndexOf((object)$ItemName$);
}
public void CopyTo($TypeName$[] $ItemName$Array, int index)
{
((ICollection)this).CopyTo($ItemName$Array, index);
}
public $TypeName$ this[int index]
{
get { return ($TypeName$)((IList)this)[index]; }
set { ((IList)this)[index] = value; }
}
#endregion Implemantation
#region IEnumerable Members
public new IEnumerator GetEnumerator()
{
foreach ($TypeName$ $ItemName$ in InnerList)
{
yield return $ItemName$;
}
}
#endregion IEnumerable Members
#region Overrides
protected override void OnValidate(object value)
{
base.OnValidate(value);
if (!(value is $TypeName$))
{
throw new ArgumentException(”This type only supports objects of this type.”);
}
}
#endregion Overrides
}$end$]]>

Matt, good post. For completeness, you should probably implement int IndexOf(Person person); as well. In fact FxCop 1.30 will complain if you don’t.