The Ownership Relationship - Implementing Collections

Ownership really defines a parent-child type relationship between objects. There may be one child, or this may be a one-to-many relationship. In this section, we're going to focus on the situation where one parent has many children.

If there can only be one child, it's often more appropriate to view the relationship as a form of aggregation, which we'll talk about later in this chapter.

This ownership relationship is the basis for most object hierarchies we'll find in Windows applications. It is usually viewed as a collection of child objects that are owned or controlled in some way by the parent. The best way to manage this type of relationship is to build a collection class to contain the child objects, and then implement a read-only property on the parent object to provide access to the collection object.

The Invoice Object

If we consider our video store example, our Invoice object is likely to have a list of line items: one line item for each video that the customer rents. This is a parent-child type relationship, where it would be good to have a collection of LineItem objects available through the Invoice object.

In our Invoice object, we'll add a property so that the calling program can access the collection of videos. We'll call the new property LineItems, since it will be a list of LineItem objects. The code to handle this in the Invoice class would look something like this:

Option Explicit

Private WithEvents objLineItems As InvoiceItems

Private Sub Class_Initialize()
  Set objLineItems = New InvoiceItems
End Sub

Public Property Get LineItems() As InvoiceItems
  Set LineItems = objLineItems
End Property

Private Sub objLineItems_AddItem(strID As String)
  CalculateAmounts
End Sub

Private Sub objLineItems_RemoveItem(strID As String)
  CalculateAmounts
End Sub

Private Sub CalculateAmounts()
  ' here we'd recalculate the subtotal, tax
  ' and total amounts for the invoice
End Sub

The LineItems property is the key, since it allows the calling program to get at the InvoiceItems collection. We've also included events so that the Invoice object will be notified when a LineItem object is added or removed from the collection. This ties in with our earlier discussion about raising events to recalculate the subtotal, tax and total amounts on the invoice.

The InvoiceItems Collection Object

The InvoiceItems collection class is based on Visual Basic's native Collection class. Our collection uses a Visual Basic Collection object, internally, to store all the LineItem objects - but we've enhanced the Add method so that it only supports LineItem objects.

The normal Visual Basic Collection will accept just about anything to store, which doesn't bode well if we only want to get LineItem objects.

Here's what the code to support our InvoiceItems collection object might look like:

Option Explicit

Event AddItem(strID As String)
Event RemoveItem(strID As String)

Private colItems As Collection

Private Sub Class_Initialize()
  Set colItems = New Collection
End Sub

Public Function Add(strID As String) As LineItem
  Dim objItem As LineItem
  
  Set objItem = New LineItem
  objItem.Load strID
  colItems.Add objItem, strID
  Set Add = objItem
  RaiseEvent AddItem(strID)
End Function

Public Sub Remove(ByVal varIndex As Variant)
  Dim strID As String
  
  strID = colItems.Item(varIndex).ID
  colItems.Remove varIndex
  RaiseEvent RemoveItem(strID)
End Sub

Public Function Count() As Long
  Count = colItems.Count
End Function

Public Function Item(ByVal varIndex As Variant) As LineItem
  Set Item = colItems.Item(varIndex)
End Function

Public Function NewEnum() As IUnknown
  Set NewEnum = colItems.[_NewEnum]
End Function

In this code, we've simply encapsulated a Collection object, colItems, by creating a Private variable and exposing our own Add, Remove, Item, and Count elements.

The key here is that the new Add method only supports LineItem objects, so that no unexpected object types can get into our collection. The Add method is also responsible for creating the LineItem object, and arranges to load any data based on the ID value supplied - by calling the Load method.

  Set objItem = New LineItem

  objItem.Load strID

Although we can't see it in the code, the Item method has been made the default by using a new capability of Visual Basic 5.0. This means that where we'd previously have written code like this:

  Set objLineItem = objItems.Item(1)

We can now write the same line like this:

  Set objLineItem = objItems(1)

To set a method as the default,

choose the Tools-Procedure Attributes menu, and click on the Advanced button when the dialog comes up:

In the Name field, choose the method name to be changed. Then, in the Procedure ID field, choose the (Default) setting and click OK.

Our new class also contains a NewEnum method. This is a special method that allows the calling code to use the For Each...Next style of accessing the contents of our collection. This means that we can write client code like this:

  For Each objLineItem In objItems

    Debug.Print objLineItem.Price

  Next

Until Visual Basic 5.0, the For Each structure was only available for native Visual Basic Collection objects. Now, by implementing the NewEnum method, we can make our user-defined Collection objects support the For...Each structure as well.

Our NewEnum method simply returns a hidden object within the native Visual Basic Collection class, called _NewEnum. Like the Item method, we need to use the Tools-Procedure Attributes menu option to set some special values for the NewEnum method. The following diagram shows the settings that are required:

The Procedure ID field is set to –4, which is the value required to work with For...Each structures. Also, the Hide this member box is checked, to make our method hidden in the COM type library.

Notice that the InvoiceItems class declares and raises two events: AddItem and RemoveItem. These events allow the Invoice object to know when the calling program has added or removed a video from the collection, so that the invoice amounts can be recalculated.

The LineItem Object

We'll keep the LineItem object simple. We'll just give it an ID and a Price property, along with the Load method. Here's the code:

Option Explicit

Private strID As String
Private dblPrice As Double

Public Sub Load(ID As String)
  strID = ID
  ' eventually there will be code here to go
  ' load the video information from the
  ' database, but for now we'll just make up
  ' a price
  dblPrice = 1.99
End Sub

Public Property Let ID(strValue As String)
  strID = strValue
End Property

Public Property Get ID() As String
  ID = strID
End Property

Public Property Let Price(dblValue As Double)
  dblPrice = dblValue
End Property

Public Property Get Price() As Double
  Price = dblPrice
End Property

The LineItem object has no extra code to support collections or any other relationships. The object is entirely designed around modeling the real-world video entity.

The Calling Program

In order to bring this whole thing together, let's look at some code that will add some line items to the invoice and then interrogate the price of the first item:

  Dim objInvoice As Invoice

  Dim strPrice as String

  

  Set objInvoice = New Invoice

  objInvoice.LineItems.Add "1"

  objInvoice.LineItems.Add "2"

  strPrice= objInvoice.LineItems(1).Price

Notice how we've just used the InvoiceItems method as though it were a collection, calling its Add method for each video.

This approach to implementing parent-child relationships is very powerful, and yet it provides a very easy interface for the end programmer who'll be using our objects to develop the application.