Get organized: Take advantage of VB custom collections

Developers often need places to stash their objects. Lamont Adams explains how to create and use custom collection classes in VB and points out some situations when they are useful.

Q: How do I go about creating a custom collection, why would I want to, and what is a good example of its use?


A: Well, VB's Class Builder Wizard can create one for you, but of course, real programmers don't use wizards (or eat quiche). Luckily, creating a custom collection isn't terribly complicated, as you’ll see in this article. First though, let's backtrack a bit and define a custom collection and answer the second part of your question.

VB's Collection objects manage themselves, growing and shrinking as needed when you add items to them or remove items from them. By taking advantage of their keying feature and specifying a String key when adding an object, you can make retrieving an arbitrary object from a collection relatively fast. So a collection offers a simple way of organizing and storing objects in your applications.

You could create a public collection and add objects to it as needed. The problem with this approach is that since you are just exposing the collection, you can't control what is added to it. Suppose you are using a collection to store a bunch of cFoo objects and someone adds a cBar object to it by mistake. Any code that looks at that collection later, expecting only cFoo objects to be in it, will likely freak out when it discovers that stowaway cBar object.

Another problem with using a public collection is that the object being stored in your collection must be public and creatable by the collection's client code. That is, the cFoo class must be explicitly created by the programmer before it can be added to the collection. You couldn't, for example, create the cFoo object inside the Add method using parameters supplied by the client and provide default values for any optional ones. Nor could you create another object that itself contained the cFoo object, along with some other data about it, and then actually store that new object in your collection. In other words, your implementation is not hidden from the client code.

Here's my collection
If VB supported implementation inheritance, we'd solve the first problem by creating a child class of Collection and overriding its Add method with one of our own that accepted only cFoo objects. We can't do that of course, at least not in "classic VB." But we can fake it by creating a class that exposes all the expected methods and properties of a collection and contains a private collection it uses to store any objects it is passed.

This is actually a perfectly valid object-oriented technique known as composition, and it’s what I did with cFooCollection in Listing A, which provides a collection-like class with an Add method that will accept only cFoo objects. The cFooCollection class isn't type-compatible with Collection, which means that you can’t use it with subs and functions that expect parameters of type Collection. Aside from the extra work involved in creating it, that's really the only downside here.

We can change the code in Listing A to solve our second problem by changing the Add method to accept whatever parameters are necessary to create a new cFoo object, instead of creating the cFoo object itself, as in Listing B.

What's this "NewEnum" thingy?
Looking at either of my cFooCollection classes, you can see that they have all the methods and properties you'd expect of a real collection: Add, Item, Count, Clear, and Remove. The Item method is even set as the default method, so you could use the following syntax to access a hypothetical SomeMethod method of the first cFoo object in the collection:

You'll also notice a Get property in the source called NewEnum, which provides support for VB's For…Each construct. NewEnum is a hidden member of the VB Collection class's interface that handles retrieving objects from the collection during an iteration using For…Each. Any collection class that is to support For…Each enumeration has to provide this property with that exact implementation, plus some hidden things you can't see in the code. I'll explain these hidden things shortly.

I mentioned above that cFooCollection's Item method is the default method of the class, but I didn't say how I did that. Since it’s done in a similar manner to the hidden settings that NewEnum requires, I'm going to kill two birds with one stone. Both settings require the use of the Procedure Attributes dialog, which you’ll find under the Tools menu. When the dialog appears, click Advanced. Figure A shows this dialog with the properties of NewEnum listed.

Figure A
Click the Advanced button in the Procedure Attributes dialog to access these options.

To make a sub or function the default member of a class, you select the (Default) item from the Procedure ID combo box. You should do this for the Item Function. The NewEnum property, on the other hand, requires a Procedure ID of –4. You can just type this into the field.

What good is this?
To answer the final part of your question, I can provide a personal anecdote that illustrates a good use of a custom collection. A project I was involved with a few years ago needed access to two databases: one on each client machine and the other residing on an AS/400. This was not long after Microsoft had released ADO, but the immaturity of that new database access library and numerous bugs that were being reported about it led us to stick with DAO for our client database connectivity and a proprietary software package for our AS/400 connectivity.

However, we wanted to be able to "upgrade" to ADO at a later date. We also wanted to leave the way clear for eventual upsizing of the application and the possible creation of a Web interface for it. So, in keeping with good tiered-application design practices, our solution had three tiers: one for the UI, one to provide data access, and a middle-tier business rules layer.

That middle tier made extensive use of custom collection classes to organize data from our databases into object form. We used these for two main reasons:
  1. ·        We wanted to insulate as much of the application as possible from our database access technologies, which were subject to change. The middle tier would accept basic objects created by the data tier and combine them into collection classes where appropriate.
  2. ·        We did not want the UI layer to know about the basic, private, internal classes used by the middle tier and data access layers. This meant that we couldn't make the classes we were storing in most of our collection public and creatable by the UI layer.

That wraps up our look at how to create custom collections, along with some reasons why you should. Until next time, may all your ActiveX DLL's remain binary compatible.



Editor's Picks