Home
Foreword
Preface
Class Idioms
Collections
Implements
Constructors
Terminate
Forms
On Error
Frameworks
F.A.Q.
Value-added
FSMs
Constants
GOTO
Hungarian
Nothing
Properties
Big EXEs

A useful VB idiom is the single-form class, which we can implement using a form module in place of a class. This is useful in any situation where we need a class that has a single-form user interface, and the reason it's so useful is that it dramatically improves cohesion by keeping everything in one file. It also reduces the number of source files to clutter up the project. A prime candidate for implementation as a single-form class is a customised collection - we can use such a collection to store a list of alternatives and then present the list for a user to select from, via a Choose method:

x = oNames.Choose  ' Populates a listbox from the collection
                   ' and shows the form so the user can
                   ' choose something

Collection classes are described in the Visual Basic Programmer's Guide (search for 'house of bricks' in Books Online), and a seamless implementation relies on the provision of two standard methods, Item and NewEnum. The important thing is that we are able to set the Procedure Ids for these two methods via the Tools/Procedure Attributes menu; the result is that we're able to use two standard collection-like behaviours with our custom collection:

    x = oMyCollection("keyval")

and

    For Each oThing In oMyCollection
        ...
    Next oThing

The first of these behaviours relies on the Item method being the default method of our class, while the second implicitly invokes the NewEnum method; both of these are achieved by assigning standard procedure ids. (These procedure ids are just a bit of COM magic - see the Programmer's Guide for details.)

This all works great if we use a class module, but trying to define the default method of a form doesn't work, reulting in the curiously ungrammatical error 'Member already exists in a object module from which this object module derives'. A further problem is that we can't define a Count property, another standard fixture of collection types, because forms already have such a property.

Looking on the bright side, the enumerator works and if we're prepared to use a non-standard name for the Count property and to access the Items property by name, we can limp along with our custom collection. But maybe we can find a better way. The real problem is that we're creating an ActiveX interface for our class but it's rubbing shoulders with an existing interface, Form, which every form object has built in.

Unfortunately VB doesn't separate the two interfaces as cleanly as it should. A form is logically a class that Implements Form, but if the Implements model was applied we'd need to access a form's built-in properties and methods through a variable of type Form. Any custom properties and methods would be accessed through a different data type (Form1, if we accept the default name), which would be cumbersome in practice. And what would TypeName(Me) return? It looks like the interface confusion may be for reasons of compatibilty with earlier versions of Visual Basic.

The clear way around this problem is to add the collection interface to our form using the proper Implements mechanism, since this keeps it clear of the Form interface. Implements Collection isn't allowed, unfortunately, but we can define our own class, CCollection, and use that as our interface. We'll actually write CCollection as a collection class and delegate to it from our forms. Here's the basic class:

Basic Collection Class
CCollection

We can use this class on its own to make collection variables, but that doesn't add much to the standard Collection type (the IsMember method is a useful addition, since the only way to check if a key is in a collection already is to look it up and trap the error). We'll use this class primarily to derive new collection classes from; we provide an implementation here instead of just an interface so we can delegate from our derived classes to encapsulated instances of CCollection.

Private m_colData As Collection

Public Function Item(ByVal Key As Variant) As Variant
    Item = m_colData(Key)
End Function

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

Public Sub Add(ByVal Thing As Variant, _
               Optional ByVal Key As Variant)
    m_colData.Add Thing, Key
End Sub

Public Sub Remove(ByVal Key As Variant)
    m_colData.Remove Key
End Sub

Public Function IsMember(ByVal Key As String) As Boolean
    ' Implementation omitted
End Function

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

Now let's look at a class that derives from CCollection. Recall that we're going to implement this as a form, and that we want to include a Choose method so we can display the collection contents and allow the user to select from it. For this example we'll just make our collection store strings, since we can use these directly when filling the listbox.

Derived Collection Class
CNames

The CNames class is built using a Form instead of a Class module. This code assumes a single listbox control, and the user selects by double-clicking (this dodges a couple of issues, but the principle is valid).

Implements CCollection

Private m_oData As CCollection
Private m_sSelected As String

Private Sub CCollection_Remove(ByVal Key As Variant)
    m_oData.Remove Key
End Sub

Private Sub Form_Initialize()
    Set m_oData = New CCollection
End Sub

Private Sub CCollection_Add(ByVal Thing As Variant, _
                       Optional ByVal Key As Variant)
    m_oData.Add Thing, Key
End Sub

Private Function CCollection_Item(ByVal Key As Variant) As Variant
    CCollection_Item = m_oData(Key)
End Function

Private Function CCollection_NewEnum() As IUnknown
    Set CCollection_NewEnum = m_oData.NewEnum
End Function

Public Function Choose() As String
    lstNames.Clear
    Dim vName As Variant
    For Each vName In m_oData
        lstNames.AddItem vName
    Next vName
    Me.Show vbModal
    Choose = m_sSelected
End Function

Private Sub Form_Resize()
    lstNames.Move 0, 0, Me.ScaleWidth, Me.ScaleHeight
End Sub

Private Sub lstNames_DblClick()
    m_sSelected = lstNames.Text
    Unload Me
End Sub
 

Now we have a collection class, CNames, with a form built into it. We can use this class in the same way as we use built-in collections (of type Collection), but we can also invoke the Choose method to display the collection contents. There is a trade-off in usability, however, since we need to use two different interfaces to get to all the properties and methods: Choose is part of the CNames interface, while all the other properties and methods are provided by CCollection (or Form - but, as we've seen, these are actually available through the CNames interface). The coding issues with manipulating multiple interfaces on a single object are covered in Chapter 3, but the best approach uses a global function to convert CNames reference into a CCollection reference:

Public Function CCCollection( _
            ByVal oThing As CCollection) As CCollection
    Set CCCollection = oThing
End Function

This needs to be defined in a BAS file (or a global class - see Chapter 1 for details). Here's some code to exercise the CNames class:

Using the Derived Class

' Create the collection

Dim oNames As CNames
Set oNames = New CNames

' Fill it with data. Note the use of the interface
' conversion function.

CCCollection(oNames).Add "Fred"
CCCollection(oNames).Add "Wilma"
CCCollection(oNames).Add "Barney"
CCCollection(oNames).Add "Betty"
CCCollection(oNames).Add "Dino"
CCCollection(oNames).Add "Pebbles"
CCCollection(oNames).Add "Bam-Bam"

' Call the Choose method. No interface conversion
' is necessary since our variable is of type CNames.

MsgBox "You chose " & oNames.Choose

' Now exercise the enumerator function. We need the
' conversion function again, as the enumerator belongs
' to the CCollection interface.

Dim vName As Variant
For Each vName In CCCollection(oNames)
    MsgBox vName
Next vName
 

This works great for collections of strings, but to make our Choose method more general, in practice we may want to define a CCollectionMember interface. This interface would include a string property, say DisplayName, which is what we'd show in the listbox.
 

Key Spinner

© 1998 - 2009 Mark Hurst. All rights reserved.   Updated March 01, 2009