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.
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 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.
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.
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.