Visual Basic's Collection object lets you combine objectsor items of any data type for that matterinto a set that can be referred to as a single unit. You might think of a Collection object as an extremely flexible array that contains just about anything you want to put in it, including objects and other collections. The most common use for Collection objects, however, is to maintain a collection of an unspecified number of instances of a specific type of object. That's what the following example will demonstrate.
There are many ways to use collections in the Visual Basic objects you create. Some techniques are more "dangerous" than others, and depending on how much you want to protect yourself or other programmers, you can implement collections of objects in simple ways or in somewhat intricate but safe configurations. I recommend that you thoroughly read the Books Online topics covering this subject to gain a solid understanding of Microsoft's suggested approach. I've used an only slightly modified version of Microsoft's suggested techniques, and I think it would be helpful for you to follow their guidelines, too. (Search Books Online for House of Straw, House of Sticks, and House of Bricks to locate these topics.)
The following example presents a simple, straightforward method
for implementing collections of user-defined objects within other objects.
This method is fairly robust, and the technique can be easily duplicated or
modified as desired. We'll create a solar system structure comprising a top-level Star
object that contains a Planets collection. Each Planet object within this
collection contains a Moons collection, and each Moon object is a very simple
object consisting of just a few properties.
SolarSysA Collections Example
Start up Visual Basic, and open a new Standard EXE project. Set the form's Name property to SolarSys, and save this form as SOLARSYS.FRM. Save the project using the name SOLARSYS.VBP. Change the form's Caption property to Collections Example, and add a command button named cmdBuildSolarSystem. Edit the button's Caption property to read Build Solar System. Add the following code to the form, and then save your work:
Option Explicit
Public starObject As New Star
Private Sub cmdBuildSolarSystem_Click()
`Prepare working object references
Dim planetObject As Planet
Dim moonObject As Moon
`Add planets and moons to the solar system
With starObject.Planets
`Create first planet
Set planetObject = .Add("Mercury")
`Set some of its properties
With planetObject
.Diameter = 4880
.Mass = 3.3E+23
End With
`Create second planet
Set planetObject = .Add("Venus")
`Set some of its properties
With planetObject
.Diameter = 12104
.Mass = 4.869E+24
End With
`Create third planet
Set planetObject = .Add("Earth")
`Set some of its properties
With planetObject
.Diameter = 12756
.Mass = 5.9736E+24
`Add moons to this planet
With .Moons
`Create Earth's moon
Set moonObject = .Add("Luna")
`Set some of its properties
With moonObject
.Diameter = 3476
.Mass = 7.35E+22
End With
End With
End With
`Create fourth planet
Set planetObject = .Add("Mars")
`Set some of its properties
With planetObject
.Diameter = 6794
.Mass = 6.4219E+23
`Add moons to this planet
With .Moons
`Create Mar's first moon
Set moonObject = .Add("Phobos")
`Set some of its properties
With moonObject
.Diameter = 22
.Mass = 1.08E+16
End With
`Create Mar's second moon
Set moonObject = .Add("Deimos")
`Set some of its properties
With moonObject
.Diameter = 13
.Mass = 1.8E+15
End With
End With
End With
End With
`Disable the command button
cmdBuildSolarSystem.Enabled = False
`Display the results
Print "Planet", "Moon", "Diameter (km)", "Mass (kg)"
Print String(100, "-")
For Each planetObject In starObject.Planets
With planetObject
Print .Name, , .Diameter, .Mass
End With
For Each moonObject In planetObject.Moons
With moonObject
Print , .Name, .Diameter, .Mass
End With
Next moonObject
Next planetObject
`Directly access some specific properties
Print
Print "The Earth's moon has a diameter of ";
Print starObject.Planets("Earth").Moons("Luna").Diameter;
Print " kilometers."
Print "Mars has";
Print starObject.Planets("Mars").Moons.Count;
Print " moons."
End Sub
Figure 5-7 shows the SolarSys form during development.
Figure 5-7. The SolarSys form, used to demonstrate nested collections of objects.
We'll refer back to this code in a minute, but first let's define the
objects and collections of objects that this code creates and manipulates.
The Star Class
From the Project menu, choose Add Class Module, and double-click the Class Module icon in the Add Class Module dialog box. Set the class module's Name property to Star, add the following code , and then save the module as STAR.CLS.
Option Explicit
Public Name As String
Private mPlanets As New Planets
Public Property Get Planets() As Planets
Set Planets = mPlanets
End Property
The Star object declares and contains a collection of planets. We'll take a look at the Planets class in a minute, but for now be aware that the Planets class defines a collection of Planet objects, which are in turn defined in their own class.
Notice that the Planets collection is declared private, as
mPlanets, so it's not directly accessible by the external world. Instead, the Property Get
Planets procedure provides read-only access to the collection. Since there's
no corresponding Property Let procedure, external procedures are prevented
from reassigning unexpected objects to this reference. We need the read-only
access to the Star's Planets collection in order to access the individual Planet
objects it contains.
The Planets Class
All the planets in the solar system will be maintained as individual Planet objects in a Planets collection. Notice that I named this collection the same as the objects in it, except for the addition of the natural-sounding "s" to make it plural. This is the standard technique used throughout all object collections. Add a new class module to your project, name it Planets, add the following code to it, and save it as PLANETS.CLS.
Option Explicit
Private mcolPlanets As New Collection
Public Function Add(strName As String) As Planet
Dim PlanetNew As New Planet
PlanetNew.Name = strName
mcolPlanets.Add PlanetNew, strName
Set Add = PlanetNew
End Function
Public Sub Delete(strName As String)
mcolPlanets.Remove strName
End Sub
Public Function Count() As Long
Count = mcolPlanets.Count
End Function
Public Function Item(strName As String) As Planet
Set Item = mcolPlanets.Item(strName)
End Function
Public Function NewEnum() As IUnknown
Set NewEnum = mcolPlanets.[_NewEnum]
End Function
When I first started creating collections of objects, I was tempted to combine each object and its collection into one class module. This works, but there are lots of hidden problems and unforeseen gotchas that are avoided by making the collection an object separate from the objects it contains. That's why this example application includes both a Planet and a Planets class. They work hand in hand to create a fairly robust collection of planets. Likewise, as you'll see in a minute, the Moon and Moons classes work together to create a collection of moons.
The Planets collection class encapsulates and handles the creation and manipulation of individual Planet objects. The actual Collection object, mcolPlanets, is declared private to this class module to encapsulate and protect it from the outside world. The Add, Delete, Count, Item, and NewEnum collection methods provide the interface used to interact with the collection of planets. I've used some special tricks you should know about, so let's go over these procedures to explain them.
The Add method is used to create a new planet in the collection. For this example I've used the name of each planet, as passed to the Add method, both to set the planet's Name property and as the key by which the planet is to be stored and retrieved. The key can be something different from the name, as it is in the collections example described in Visual Basic's online documentation. Each element stored in the collection must have a unique string key, and letting the collection class create and automatically maintain its own set of keys is one way to guarantee each key's uniqueness. The technique I've chosen is slightly simpler to implement and provides a natural way to retrieve each planet from the collection by name. Note, however, that an error will be generated if an attempt is made to add two planets of the same name. You may want to add code to the Add method to trap errors caused by repeated planet names. To keep things simple I chose to leave this step up to the reader.
The Count procedure returns the number of planets in the Planets collection, and the Delete procedure removes a planet from the Planets collection. Again, you may wish to add error-trapping code to prevent errors resulting from attempts to delete nonexistent planets from the collection.
When given a specific planet name, the Item procedure returns that planet object from the collection. Here's the first important trick I want to mention. If you set the Item procedure as the default property of the class, then the syntax to access a specific planet is more natural. For example, you can access the planet Earth object using the Item property like this: starObject.Planets.Item("Earth"); or, if the Item procedure is set as the default procedure, the syntax is shortened to this: starObject.Planets ("Earth"). This shortened syntax is much more in keeping with the standard object syntax you'll see in many other Windows application objects.
NOTE
To set a procedure as the default, select Procedure Attributes from the Tools menu. Select the procedure in the Name drop-down list, click the Advanced button to see more options in the dialog box, and select (Default) from the Procedure ID drop-down list. Only one procedure in each class can be selected as the default.
The second important trick enables you to access objects in a collection class using the For Each looping construct. This one's a little obscure, so follow along closely. The first step is to add the special NewEnum IUnknown procedure to your collection class, as shown in the code for the Planets class. The square brackets are required, as is the underscore for the hidden enumeration property of Visual Basic's Collection object. The second step is to hide and enable the NewEnum function in your class, using the steps detailed in the following note:
NOTE
To hide and enable the special NewEnum procedure in your class, choose Procedure Attributes from the Tools menu. Select the NewEnum procedure in the Name drop-down list, and click the Advanced button to see more options. Click to put a check mark in the Hide This Member check box, and type the special code number of minus four (-4) in the Procedure ID list. This enables your collection for access using the For Each looping construct.
Add another class module to the SolarSys project, set its Name property to Planet, and save the module as PLANET.CLS. This class defines the individual Planet objects that are collected within the Planets collection. Add the following code to this class module.
Option Explicit
Public Name As String
Public Diameter As Long
Public Mass As Single
Private mMoons As New Moons
Public Property Get Moons() As Moons
Set Moons = mMoons
End Property
Each Planet object has a few simple properties (Name, Diameter, and
Mass) and a Moons collection containing anywhere from zero to several
Moon objects. Once again, the actual collection is declared private so that external
code won't be able to set unexpected references into the Moons object reference.
The read-only Property Get Moons procedure allows you to reference the
moons for this planet.
The Moons Class
The Moons class is a collection class very similar in structure to the Planets collection class. Add another class module to your SolarSys project, set its Name property to Moons, add the following code to it, and save it as MOONS.CLS.
Option Explicit
Private mcolMoons As New Collection
Public Function Add(strName As String) As Moon
Dim MoonNew As New Moon
MoonNew.Name = strName
mcolMoons.Add MoonNew, strName
Set Add = MoonNew
End Function
Public Sub Delete(strName As String)
mcolMoons.Remove strName
End Sub
Public Function Count() As Long
Count = mcolMoons.Count
End Function
Public Function Item(strName As String) As Moon
Set Item = mcolMoons.Item(strName)
End Function
Public Function NewEnum() As IUnknown
Set NewEnum = mcolMoons.[_NewEnum]
End Function
Notice the striking similarity to the Planets collection class. The
procedures are nearly identical; only the referenced object names have been changed
to protect the innocent.
The Moon Class
This class is ultrasimple. Each Moon object will have Name, Diameter, and Mass properties, and that's all. Add another class module to the project, set its Name property to Moon, save it as MOON.CLS, and add the following code to define each Moon object:
Option Explicit Public Name As String Public Diameter As Long Public Mass As Single
Let's take a closer look at the top-level code in the SolarSys form. When the Build Solar System command button is clicked, a new Star object, named starObject, is created. Since we aren't concerned with creating more than one Star object, we won't bother to set its Name property. (If we were creating a program to catalog all star systems in a collection of science fiction stories, we might want to create multiple Star objects, perhaps in a Stars collection, and unique names for each would probably be appropriate.)
To simplify the code, I added only the first four of our solar system's planets and their moons. Feel free to build up the whole solar system if you feel so inclined! The following code adds the first Planet object named Mercury. Mercury has no moons, so we won't add any of these objects to this planet:
`Add planets and moons to the solar system
With starObject.Planets
`Create first planet
Set planetObject = .Add("Mercury")
`Set some of its properties
With planetObject
.Diameter = 4880
.Mass = 3.3E+23
End With
I've used the With statement to simplify the object references because we're going several layers deep here. However, we could directly access the planet Mercury object's properties by explicitly using dot notation to get to them. For example, here's what the line to set Mercury's Diameter property would look like using the complete dot notation:
starObject.Planets("Mercury").Diameter = 4880
Paraphrased, this says to set the value 4880 as the Diameter property of the planet Mercury object in the Planets collection maintained by the Star object named starObject. Whew! You can see why the With statement is useful. In addition to being easier for humans to comprehend, With blocks reduce the number of de-referencing steps required to access each object and its properties, thus speeding things up.
The next block of code repeats the process, adding the second Planet object, named Venus, which also lacks Moon objects. Let's skip ahead then to the third planet from the sun, named Earth, which does have one moon. Here's the code to add this planet and its moon to the nested collections:
`Create third planet
Set planetObject = .Add("Earth")
`Set some of its properties
With planetObject
.Diameter = 12756
.Mass = 5.9736E+24
`Add moons to this planet
With .Moons
`Create Earth's moon
Set moonObject = .Add("Luna")
`Set some of its properties
With moonObject
.Diameter = 3476
.Mass = 7.35E+22
End With
End With
End With
Once again, notice how the nested With constructs help in referencing the structure of nested object collections.
Once this simplified model of our solar system is built up as a set of collections of objects that are contained within other collections of objects, the next few lines of code access this structure to display the names of all the nested objects:
`Display the results
Print "Planet", "Moon", "Diameter (km)", "Mass (kg)"
Print String(100, "-")
For Each planetObject In starObject.Planets
With planetObject
Print .Name, , .Diameter, .Mass
End With
For Each moonObject In planetObject.Moons
With moonObject
Print , .Name, .Diameter, .Mass
End With
Next moonObject
Next planetObject
We've enabled the For Each looping construct, described earlier in this chapter, which greatly aids in the process of referencing the various properties of the planets and moons. The code above is fairly straightforward and easy to understand. For purposes of comparison, in the next few lines of the code I've used some direct property referencing using the full dot notation to access and print some properties:
`Directly access some specific properties
Print
Print "The Earth's moon has a diameter of ";
Print starObject.Planets("Earth").Moons("Luna").Diameter;
Print " kilometers."
Print "Mars has";
Print starObject.Planets("Mars").Moons.Count;
Print " moons."
Figure 5-8 shows the results as displayed on the SolarSys form after the command button is clicked.
Figure 5-8. The simplified solar system's Planets and Moons collections.
There are a lot of ways you could modify the technique presented here to suit your needs. As mentioned, you might want to add some code to prevent accidental attempts to add multiple objects of the same name and to handle missing objects in a more robust fashion. This method serves as a strong conceptual foundation on which to build without getting too lost in all the possible complexities. For many programming situations, this method can provide everything you need.