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