The fact that most of the collections classes in the .NET Framework class library are designed to work with the System.Object type makes them very versatile. However, this flexibility can also be viewed as a minor problem, since it means you have to do a lot of casting (assuming you know the type in the first place). For example, assuming a Hashtable contains a number of items of the type Product , you have to cast each item you retrieve. Using C#, you would write:
Hashtable products; Product p; p = (Product) product["ProductCode"];
Using VB.NET, you would write:
Dim products As Hashtable Dim p As Product p = CType(products("ProductCode"), Product)
Casting types like this isn't difficult, but it can make code less readable, and often leads to silly compiler errors when you forget to cast. Strongly typed collections can resolve these problems. Rather than directly using the collection classes such as Hashtable in your class, you should instead provide a custom type that encapsulates the collection type being used, and also removes the need for casting.
For example, using C# you would write:
ProductCollection products; Product p; p = products["ProductCode"];
Using VB.NET, you would write:
Dim products As ProductCollection Dim p As Product p = products("ProductCode")
Implementing a strongly typed collection like this is relatively simple, and it allows you to build in additional rules and error handling, saving you from duplicating them throughout the code.
To a strongly typed collection, you have to:
Define the custom type of the item held in the collection.
Create the collection class, and implement the Add , Remove , and GetEnumerator methods , as well as the Item property.
Let's look at each of these in turn .
In the strongly typed example, a Product type was used. When implementing a collection, you should always have a custom type, and a collection for that custom type in which the collection name is simply the custom type name appended with Collection; for example, Product and ProductCollection , or Address and AddressCollection .
Here is a class definition for a Product type, written using VB.NET:
Public Class Product ' private fields Private _code As string Private _description As string Private _price As Double ' constructor Public Sub New(initialCode As String, _ initialDescription As String, _ initialPrice As Double) Code = initialCode Description = initialDescription Price = initialPrice End Sub Public Property Description As String Get Description = _description End Get Set _description = Value End Set End Property Public Property Code As String Get Code = _code End Get Set _code = Value End Set End Property Public Property Price As Double Get Price = _price End Get Set _price = Value End Set End Property End Class
The Product type has three public properties:
Code : A unique code assigned to the product.
Description : A description of the product.
Price : The cost of the product.
All of the properties have a get and set accessor, and their implementation simply stores or retrieves the property value in a private field. The field name is the same as the property name, but prefixed with an underscore . The Product type has a constructor that accepts three parameters, allowing quick initialization. For example:
Dim p As Product p = New Product("PROASP3", "Professional ASP 3.0", 39.99)
For the purposes of this example, let's define the classes within the ASP.NET page. Typically, you would define these in a separate compiled assembly. That topic was introduced in Chapters 3 and 4, and is covered in more detail in Chapter 17.
The ProductCollection class will support two key features:
Unordered enumeration of all the contained products.
Direct access of a product using a product code.
Since the Hashtable class provides the collection functionality necessary for implementing these features, you can use a Hashtable internally within your collection class for holding items. Then, you can aggregate the functionality of Hashtable and expose it, to provide access to your items in a type safe way that doesn't require casting.
Since an internal Hashtable is being used to hold the Product items, define a private field called _products of type Hashtable within the collection class. A new object instance is assigned to this field in the constructor:
Dim _products as Hashtable Public Sub New() _products = New Hashtable() End Sub
The Add method allows a new Product to be added to the collection:
Public Sub Add( Item as Product ) If Item Is Nothing Then Throw New ArgumentException("Product cannot be null") End If _products.Add(Item.Code, Item) End Sub
This method throws an ArgumentException if a null item parameter is passed. If the parameter is not null , the Code property of the passed Product is used as the key for the Product in the contained Hashtable . Depending on your requirements, you could perform additional business logic validation here and throw additional exceptions.
The Remove method removes a Product from the collection. The implementation of the method simply calls the Remove method of the Hashtable :
Public Sub Remove(Item as Product) _products.Remove(Item.Code) End Sub
The Item property allows a Product to be retrieved from, or added to the collection by specifying the product code:
Public Default Property Item(Code as String) as Product Get Item = CType(_products(Code), Product) End Get Set Add(Value) End Set End Property
The implementation of the Set accessor calls the Add method in order to add the new product to the internal Hashtable . The process is implemented in a way so that any business logic in the Add method (such as the Null check) is neither duplicated nor missed.
To use your collection class with the for..each statements in VB.NET and C#, your collection class must have a method called GetEnumerator . Although not strictly necessary, the IEnumerable interface is also implemented using the VB.NET Implements keyword. This is good practice and requires very little work:
Public Class ProductCollection Implements IEnumerable ' ... ' implement an enumerator for the products Public Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator GetEnumerator = _products.Values.GetEnumerator() End Function ' ... End Class
The GetEnumerator method has to return an enumerator object that implements the IEnumerator interface. You could implement this interface by creating another class, but since your collection class is using a Hashtable internally, it makes much more sense to reuse the enumerator object provided by that class when its values are enumerated. The Hashtable.Values property returns an ICollection interface and since the ICollection interface derives from IEnumerable , you can call GetEnumerator to create an enumerator object for the collection of values.
With the Product and ProductCollection classes created, you can use them just like the other collections in this chapter, but this time with no casting. For example:
' Page-level variable Dim _products as ProductCollection = New ProductCollection Sub Page_Load(sender as Object, events As EventArgs) ' Runs when page is loaded Dim products As New ProductCollection() Dim p As product p = New Product("CAR", "A New Car", 19999.99) products.Add(p) p = New Product("HOUSE", "A New House", 299999.99) products.Add(p) p = New Product("BOOK", "A New Book", 49.99) products(p.Code) = p For Each p In products outResult1.InnerHtml &= p.Code & " - " & p.Description _ & " - $" & p.Price & "<br />" Next products.Remove( products("HOUSE") ) For Each p In products outResult2.InnerHtml &= p.Code & " - " & p.Description _ & " - $" & p.Price & "<br />" Next outResult3.InnerHtml = "Description for code CAR is: " _ & products("CAR").Description End Sub
The complete code for this example is contained in the samples available for download, in both VB.NET and C#. The result of running this page is shown in Figure 15-16:
The DictionaryBase and CollectionBase classes allow you to create a Hashtable or ArrayList collection that can validate, and therefore restrict, the types it contains. It's a simple process to create your own collection class by deriving from these classes.
This simple ASP.NET page defines a MyStringCollection collection class, adds three strings and one integer, and then displays the contents:
'our custom collection Class MyStringCollection Inherits CollectionBase ... implementation goes here... End Class Dim names As IList names = New MyStringCollection names.Add("Richard") names.Add("Alex") names.Add("Dave") Try names.Add(2002) Catch e As Exception outResult1.InnerHtml = "Error: " & e.Message End Try For Each name As String in names outResult2.InnerHtml &= name & "<br />" Next
The Collection base class implements the IList and ICollection interfaces. All the members of these interfaces are defined explicitly, which is why in the sample code the names variables have been defined as type IList .
Each of the collection base classes provides a number of virtual functions that are called when the collection is modified. For example, OnClear is called when a collection is cleared; OnInsert is called when an item is added; OnRemove when an item is deleted, and so on. By overriding one of these methods, you can perform additional checks and throw an exception if an undesired condition arises. For example, in the collection class, you could implement an OnInsert method that throws an ArgumentException if anything other than a string is added:
Class MyStringCollection Inherits CollectionBase Overrides Protected Sub OnInsert(index as Integer, item as Object) If Not(TypeOf item Is String) Then Throw New ArgumentException("My collection only supports strings") End If End Sub End Class
Figure 15-17 shows the results of running this code:
The DictionaryBase class is used in the same way as the CollectionBase class and implements the IDictionary and ICollection interfaces.
The ReadOnlyCollectionBase class provides functionality for exposing a read-only collection. The class implements the ICollection and IEnumerable interface. The items exposed are internally held in a protected ArrayList variable called InnerList . To use this class, you have to derive your own class from it, and populate the contents of the InnerList array.
When you enumerate a collection, the enumerator objects that implement the IEnumerator interface may require expensive resources. For example, depending on how underlying items are stored, a custom enumerator could be using a database connection, or be holding temporary files on disk. In these scenarios, it is important that the enumerable object releases resources it holds as soon as possible.
Due to the non-deterministic way the CLR releases object references, any code you write that directly uses an IEnumerator interface must always check if the enumerator objects that provided the interface support the IDisposable interface. You must then call the Dispose method when you've finished with the enumerator. If you do not do this, the resources held by the enumerator may not be released for some time. When you use the for..each language statement in C# and VB.NET, this is done automatically .
When you use the IEnumerator interface directly (or any other enumerable type), if you do not know whether an enumerator object supports the IDisposable interface, always check once you have finished with it. For example, in C#, you might write:
IEnumerator e = c.GetEnumerator(); try { while (e.MoveNext()) { Foo x = e.Current; // ... } } finally { IDisposable d = e as IDisposable; if (d != null) d.Dispose(); }
If you know that an enumerator object supports IDisposable , you can call it directly:
IEnumerator e = c.GetEnumerator(); try { while (e.MoveNext()) { Foo x = e.Current; // ... } } finally { ((IDisposable)e).Dispose(); }