As readers of Builder.com know from Lamont Adams’ articles "Collective confusion: Using collections in .NET" and "Getting to know the .NET collections," the .NET Framework contains a variety of collection classes in its System.Collections namespace that make it easy to manage your data. Not only does this namespace contain collections you can use out of the box (ArrayList, SortedList, Queue, Stack, Hashtable), it also includes interfaces such as IEnumerable, IList, and Icollection that let you build your own collection classes that mimic the behavior of the Framework classes.
In addition to the out-of-the-box collection classes and interfaces you can use to roll your own, there are several abstract classes you can use to derive strongly typed collection classes. These classes, CollectionBase, DictionaryBase, and ReadOnlyCollectionBase, provide the basis for objects that can hold your domain-specific data. In this article, I’ll show you how to create strongly typed collection classes for use in the data access layer of a multitiered application.
Why use collections?
The .NET Framework and Visual Studio .NET provide three primary ways for you to represent data in your applications:
- · The DataSet (both typed and untyped) and DataTable classes in ADO.NET
- · The XmlDataDocument and XmlDocument classes in the System.Xml namespace
- · Custom classes you create with custom properties
Although ADO.NET and the System.Xml namespace provide standardized objects for representing data, custom classes have the advantage of being strongly typed. This allows you to work with objects that more closely match the underlying data while not having to deal with the ADO.NET and XML programming models. This is particularly effective in large systems when an architect designs a data access layer made up of custom classes and then distributes the assembly to other developers within the organization. Of course, there's a downside to using custom classes: They require more coding, since you’ll need to create the object-relational mapping layer that the .NET Framework lacks.
As an example, consider the Player class in VB.NET shown in Listing A. In this class, the data is represented as public fields and includes a baseball player’s name and batting statistics. As is typical, the class includes a parameterized constructor used to populate the Name field. The class also contains a RunsCreated method that computes the number of runs the player created based on his offensive statistics. Instead of using fields, the class could alternatively use properties to represent the data in order to include data validation logic in the Set blocks associated with each property.
As you can see, the class is marked with the SerializableAttribute so that the common language runtime can serialize both its public and private data for transport. This is required, for example, if you want to transfer objects of type Player from one application domain to another using .NET Remoting, or return a Player object from an XML Web Service.
You’ll also notice that the class implements the IComparable interface and its CompareTo method. As you’ll see, these will be useful when Player objects are contained in our strongly typed collection class.
Creating a custom class is relatively straightforward, but a custom class like Player represents only a single entity instead of an aggregation. Most applications that manipulate data need to work with multiple instances of the entities that make up the problem domain. That’s where a strongly typed collection class enters the scene.
How to make it work
To store a set of Player objects, a developer using the class in Listing A could simply put the objects in an array or one of the prefab collection classes previously mentioned. However, the advantages of a strongly typed collection class include its ability to hold only instances of the type you designate and to add custom functionality particular to the class it holds. This ensures that the items in the collection can be worked with efficiently, without having to cast to the correct type.
To create the strongly typed collection, you can use the power of implementation inheritance. By deriving a class from CollectionBase, ReadOnlyCollectionBase, or DictionaryBase and then overriding and shadowing various members, you can enforce the type of objects in the collection as well as take advantage of functionality Microsoft has included in the .NET Framework. For example, Microsoft uses CollectionBase as the base class for more than 30 of its own strongly typed collection classes in the .NET Framework. Each of these three classes is marked as abstract (MustInherit in VB) and, therefore, can be used only in an inheritance relationship.
The difference between CollectionBase and ReadOnlyCollectionBase is self-explanatory. However, DictionaryBase can be used when the collection needs to be stored as key-value pairs, whereas the other two simply store the objects.
You can create a strongly typed collection class for Player objects by deriving from the CollectionBase class as shown in Listing B.
You’ll notice that the class is named PlayerCollection, following the Microsoft class library design guidelines in the online help that ships with Visual Studio .NET. The keys to making PlayerCollection strongly typed are the overloaded Item and Add methods. The Item property is marked as the Default property so that it can be accessed directly, and it includes signatures to retrieve a Player both by the player’s name and the index in the collection.
The first Add method accepts an object of type Player and then adds it to the collection. The second acts as a factory and creates a new Player from the name passed in, and both adds it to the collection and returns it from the method. In both cases, the protected List property of the base class, which exposes the IList interface, is used to manipulate the collection. As a result, a client working with the collection class can perform the following operations:
Dim p As New Player("Sosa")
Dim pc As New PlayerCollection()
In addition, PlayerCollection supports several standard collection methods, including Contains, IndexOf, and Remove. In each case, the PlayerCollection class not only relies on the functionality of the base class through its List property but also adds an overloaded method that accepts the Name of a player. With IndexOf, keep in mind that you can implement a more elegant means of searching the collection (for example, a binary search), depending on how large the collection might be and the performance constraints of your application.
In some situations, you might want to include a constructor marked with the Friend keyword in your collection class. This ensures that an instance of the class cannot be created from outside the assembly in which the collection is defined. This is useful when the collection is dependent on some other object for its population.
Finally, you’ll notice that the PlayerCollection class overrides the OnValidate method. Although the class accepts only Player objects through its Add method, the CollectionBase class implements the IList interface. This interface includes Add and Insert methods that would allow a client to do the following:
Dim i As Ilist
i = c ' c is of type PlayerCollection
To stop this, the OnValidate method can test for the appropriate type using the TypeOf operator and throw an exception that prevents the object from being added to the list.
In addition to the standard functionality, you can add sorting to your collection class simply by implementing a Sort method and relying on the sorting capability of the underlying CollectionBase class. As mentioned previously, the CollectionBase class exposes a List property that exposes the IList interface used to manipulate the class. The internal storage, however, is actually accomplished using an ArrayList exposed through the protected InnerList property. Using this property allows you to take advantage of ArrayList functionality such as sorting.
The base class methods such as OnValidate won’t fire if you manipulate the InnertList property directly.
When the Sort method of the InnerList is called, it looks for the IComparable interface on the objects being sorted and uses the CompareTo method to make comparisons during the sorting operation. This is why the Player class implemented the interface. You can see from Listing A that the CompareTo method compares the Name properties of the respective objects so that the default sort is in order of Name in ascending order.
But the PlayerCollection also includes an overloaded Sort method that accepts an object of type PlayerSort. The PlayerSort class in turn implements the IComparer interface and in its constructor accepts the PlayerSort value. The Compare method of the IComparer interface then makes comparisons so that the Player objects can also be sorted by AB, H, and HR. You’ll note that the Select Case statement in the Compare method does the comparison so that the PlayerCollection is sorted in descending order when using AB, H, and HR, as is typically done when viewing baseball statistics. The overloaded Sort method passes the PlayerSort object to the Sort method of the InnerList.
A client could then sort the PlayerCollection shown above, like so:
pc.Sort() ' By Name by default
In addition to this custom behavior, keep in mind that the CollectionBase class implements the IList and IEnumerable interfaces. This means that your strongly typed collection can automatically be used in both Windows Forms and Web Forms data binding and can be iterated using the For Each (foreach in C#) syntax in VB.NET.