Tuesday 17 March 2009

Creating a Custom Collection Object

Have you ever tried to create an object in VB6 that acts like a collection? If you have, you will certainly have come up against some stumbling blocks along the way. You may well have used some rather nasty hacks to get it to work, or worse still, given up altogether and just used a plain old vanilla Collection object.

In this post I shall show you how to get around these pitfalls, including assigning a default procedure and creating an enumerator for use in For Each...Next loops. We shall follow a set of simple examples to create an object that is a collection of integers. You will be able to build upon the things you learn here to create your own objects that act like collections.

First, let's look at the VB6 Collection object. There are four actions associated with it, namely Add, Count, Item and Remove. However there's something special about the Item function.

If we look at the members of the Collection in the Object Browser we can see that the Item function is shown with a turquoise dot. This simply means that it is the default member of any Collection object. What this means is, you do not need to explicitly call the Item function to return one of the items from the Collection object. A simple code example will help demonstrate this:

Dim myColl As Collection ' Instantiate the Collection object ' and populate it with some test data. Set myColl = New Collection myColl.Add 1 myColl.Add 2 myColl.Add 3 ' The following two lines of code are functionally ' equivalent even though the second line does not ' explicitly call the .Item function: Debug.Print myColl.Item(2) Debug.Print myColl(2)

Now how do we go about implementing this behaviour in VB6 with our own object? In .NET it is easy, we simple include the word Default in the routines definition. However, this does not work in VB6. Instead we have to use the Procedure Attributes dialog box. We will come to this in a moment.

In the mean time let's create the outline of our Integer Collection object which we will be using to build upon throughout the remainder of this post. Start by adding a new Class Module to your VB6 project, call it IntegerCollection and add the following code to the code file:

Option Explicit Dim m_colIntegers As Collection Private Sub Class_Initialize() Set m_colIntegers = New Collection End Sub Public Sub Add(ByVal Item As Long) m_colIntegers.Add Item End Sub Public Property Get Count() As Long Count = m_colIntegers.Count End Property Public Property Get Item(ByVal Index As Long) As Long Item = m_colIntegers(Index) End Property Public Sub Remove(ByVal Index As Long) m_colIntegers.Remove Index End Sub Private Sub Class_Terminate() Set m_colIntegers = Nothing End Sub

We now need to make the Item property the default property, as currently if we try and implicitly access it we will, at best get a run-time error, and at worst our project won't even compile. To do this select the "Procedure Attributes..." menu item from the "Tools" menu. The dialog box that is displayed allows us to enter a description and context sensitive help to each procedure. You can add information here if you want, but what we are really interested in is currently hidden.

Click the "Advanced >>" button to display the extra options. Make sure the Item property is the currently selected procedure from the "Name" drop down at the top. You should see that the "Procedure ID" drop down (on the left hand side, about half way down the dialog box) currently says "(None)", change this to say "(Default)" then click the "OK" button to apply your changes.

Our collection object now acts just like the built in Collection object in VB6 in the sense that we do not need to explicitly call the Item property, it will be implicitly called when we include the index of the item we want in brackets. Also, it is strongly typed, meaning we can only use it to store variables of type Long. If we try and pass anything else we will get a type mismatch error. This of course means that if we get passed an object of this type, we can be sure that every item in the collection is an integer. We no longer need to worry about checking the types as Variants are no longer allowed.

So we have done it! Well, not exactly. I said that our collection acted just like the built in Collection, however there's one important difference. That difference is the For Each...Next loop. If we try and iterate through each of the items in our collection with one of these loops we will be told that our object does not support this property or method.

The reason for this is that the For Each...Next loop works by calling a hidden enumerator that returns the next item in the collection. This enumerator is of type IUnknown, one of the main building blocks of COM Interoperability. (Well actually it's of type IEnumVariant and VB6 hides the implementation details of returning the item from the collection, but we won't go into that here.)

The collection object we are using to store our integers implements this enumerator as _NewEnum. Because it starts with an underscore we have to wrap our call to it in square brackets. We can then return this as our own enumerator. Add the following code to the IntegerCollection class:

Public Property Get NewEnum() As IUnknown Set NewEnum = m_colIntegers.[_NewEnum] End Property

However, we are not done yet. We now need to tell Visual Basic that this new procedure is our enumerator. This is harder than it first may seem. As unlike the Default procedure ID there is no corresponding item for the collection enumerator in the Procedure Attributes dialog box.

All is not lost though. It's just that VB doesn't include the enumerator procedure ID. I have no idea why this is, I can only assume it is because of short sightedness on the part of the VB developers. The procedure ID we are looking for corresponds to the constant FFFFFFFC, in VB this evaluates to -4.

Therefore all we need to do is go back into the "Procedure Attributes" dialog box, select the NewEnum routine from the drop down, click "Advanced >>", delete what's in the "Procedure ID" drop down and enter -4. At this point we should also tick the "Hide this member" check box so that it is not shown in the IntelliSense dropdown for our object when accessed via COM. This creates a consistent interface for our users.

Now our object truly represents a collection. It is strongly typed, has a default routine and can work in For Each...Next loops. Here is an example of how it could be implemented:

Dim myColl As IntegerCollection Dim value As Variant ' Instantiate our collection object. Set myColl = New IntegerCollection ' Add some values to it. myColl.Add 1 myColl.Add 2 myColl.Add 3 ' We do not need to explicitly call .Item ' to return a value from our collection. Debug.Print myColl(2) ' And we can cycle through all the values ' in it using a For Each...Next loop. For Each value In myColl Debug.Print value Next

I hope you've enjoyed reading this guide and that it has helped and informed you. If you have any comments or questions please get in touch by leaving a comment below. Feel free to link to this article from your own web site or pass on the address of my blog to your colleagues and friends.


References:

  1. vbVision -- Replace the Visual Basic Collections Object!
  2. How To Use the Procedure Attributes Dialog Box
  3. Paul Lomax's Top 15 VB Tips and Tricks

No comments: