Visual Basic is an enigma. Pitched at inexperienced programmers, its evolution as a programming language has been slow, yet each new version ships with an overwhelming array of new plug-in
technology for data access and component building. On the one hand we're expected to build increasingly sophisticated distributed applications, but on the other hand we're not allowed to get too technical. VB's
rudimentary code editor pegs it as a tool for the new breed of tinkertoy developer, yet language features aimed at getting novices started simply get in the way of developers who have accumulated enough experience to do
anything useful. One such example is the behaviour of forms, which we'll look at in detail here. Forms are essentially classes - 'essentially', because they're also a little bit more than classes. We can mostly use a
form anywhere we can use a class (see Chapter 1 for some exceptions), but obviously a form can have visible attributes too. It doesn't have
to be visible: for example, we might use a form in place of a regular class module because we need a class that uses a timer control. When working with forms we also need to appreciate that the visual part of a form can be in a 'loaded' or 'unloaded' state; as we'll see, most of the problems with forms revolve around these two states.
If, as is the case with classes, we were compelled to define instances of our forms before using them, one of the most confusing form issues would cease to be
an issue. This would mean that we'd always need code like this to use any form in our program: Dim oMyForm As New Form1 oMyForm.Show vbModal Unfortunately, whether or not we
define any instances of a form, Visual Basic creates one for us. We reference this object via an implicitly-defined global variable that has the same name as the class. That last sentence deserves to be printed
in bold in Chapter 1 (or perhaps Chapter 6!) of every Visual Basic book and to be taught on the first morning of every Visual Basic training course, since it explains unambiguously why we can use the identifier 'Form1'
to mean two entirely different things in two different contexts: Form1.Show ' Form1 is an object name
Dim oNewForm As New Form1 ' Form1 is a class name Sadly, VB books and programming courses almost inevitably teach programmers to use VB's implicitly-defined forms without explaining what's
going on. Creating explicit instances of forms is usually taught as a minor 'advanced' topic, to be glossed over hurriedly when MDI applications are considered. The unfortunate consequence is that many intermediate and
'advanced' VB programmers just don't understand what they're doing. You can program without ever touching VB's implicitly-defined forms, and I suggest that you do (see
Chapter 1 if you need to be persuaded). To have a full understanding of forms we need to know about the different states a form can exist in. To do that, first we need to look at the different ways in
which class instances are created. With classes things are relatively simple: our class instance (object) either exists or it doesn't. When it comes into existence, the Class_Initialize
event handler runs, and when it is destroyed, the Class_Terminate event handler runs. There's also an issue of when the object is actually created, which differs according to how we write the code:
Dim oMyObj As CMyClass ' (1) Set oMyObj = New CMyClass ' (2)
oMyObj.SomeProperty = 2 ' (3) or Dim oMyObj as New CMyClass ' (4)
oMyObj.SomeProperty = 2 ' (5) In the first example, the object is actually created at line (2). By 'actually created' we mean that the object has memory allocated and
its Class_Initialize
event handler runs. This is fairly intuitive, as Line (1) is a simple variable definition and not an executable statement. Most importantly, we can say unambiguously that the object has been created and initialized by line (3) - effectively we can consider the
Set statement as a call to the Class_Initialize event handler. By contrast, in the second example the object isn't created until line (5), which is the first reference
to any property or method - most certainly NOT intuitive! In a real application, of course, the first reference may be pages away from the variable definition and such references are very likely to be added and deleted throughout the development of the program. As a result, there is no line we can point to and say with certainty that it creates the object. This may sound like a subtle point, but can be very important if we have a lot of slow code in the
Class_initialize event. The Dim x As New y idiom also creates another serious problem. With any reasonable programming language we'd expect the following code to fail at line (4):
Dim oMyObj as New CMyClass ' (1) oMyObj.SomeProperty = 2 ' (2)
Set oMyObj = Nothing ' (3) Debug.Print oMyObj.SomeProperty ' (4) Because line (3) has destroyed
the object, line (4) should give us an 'Object variable not set' error. What actually happens is that a new object is created at line (4), its Class_initialize
event handler runs, and then the uninitialized value of 'SomeProperty' is printed. (This process is often called 'auto-instantiation'.) The burning question is, what is Dim x As New y actually for? It
seems to me that, like VB's 'magic' forms, this is a concession aimed at making things easier for novice programmers. [Trivia Note: I once had an interviewee call me an 'anorak' for asking about the distinction
between Dim x As New y and Set x = New y. C'est la VB.] Since forms are classes, all of this applies to forms too. The important thing to remember when getting to grips with forms is that the code
portion and the visual portion are entirely separate entities; as long as we don't touch anything to do with the visual part, using forms as non-visual classes poses no problems at all. The distinction between
'visual' and 'non-visual' isn't strictly correct, since we're really talking in terms of ActiveX interfaces. When we talk about the 'visual' part of a form we really mean all the properties and methods of the ActiveX
Form interface. By contrast, any extra properties and methods we define ourselves are part of a second interface, which we name by setting the form's Name
property. So, to correct the earlier assertion, as long as we don't touch any properties or methods in the Form interface, our forms will behave just like classes. A form's visual part (the Form
interface, remember) is what we're playing with when we use the Load and Unload statements, and when we invoke the Show and Hide
methods. It's helpful to think about the visual part of a form as a separate object in its own right. If we could see the definition of this object we'd see something like this:
Dim oAnon As New Form (There's a bit of dramatic licence here as we never really have a name for this object.) This definition should ring an alarm bell, because the As New
idiom implies an auto-instantiating object. This is, in fact, what we get, and the following code exhibits much the same problem we saw demonstrated earlier for class variables: Load Form1
Form1.Caption = "Hello" Unload Form1 Debug.Print Form1.Caption Once again, what this should
do is to cause an error; what it actually does is to create a new instance of the form's visual part (but not necessarily its code part - remember that the code part and the visual part are actually distinct objects) and then load that instance, complete with re-initialized properties and a
Form_Load event. (To convince yourself of this, check out the following example program.)
Example Code: Auto-instantiated Form For this example you need two forms. Form1 has two command buttons,
and you should paste the following code into the forms. The buttons load and unload an instance of Form2, which clicking on Form1's background displays Form2's caption. The beeps just tell you when the
Form_Load and Form_Unload events are executed. Note that the the variable f, an instance of Form2, is NOT auto-instantiated - this object remains in existence during the lifetime of Form1. Despite this,
the visual part of Form2 (an anonymous object created from the Form interface) IS auto-instantiated, since we can manipulate it (though 'it' is really a new object) even after the form is unloaded.
Form1 Private f As Form2
Private Sub Command1_Click() Load f
f.Caption = "hello" f.Show End Sub
Private Sub Command2_Click() Unload f End Sub
Private Sub Form_Click()
MsgBox f.Caption End Sub
Private Sub Form_Load() Set f = New Form2 End Sub Form2
Private Sub Form_Load() Beep End Sub
Private Sub Form_Unload(Cancel As Integer) Beep Beep
Beep Beep Beep Beep Beep Beep End Sub |
The Visual Basic Programmer's Guide (you can read it in Books Online if you don't have a printed copy) has a section called 'Life Cycle of Visual Basic Forms' that's required reading if you
want to learn about forms. This describes the five different states a form can be in, and some of the ways in which transitions between these states are provoked. Four of the states
correspond to things we've already discussed here, and the fifth one is a pathological state that shouldn't concern us in day to day programming. We've already seen the most damaging problem with forms, which is
that the visual part of the form is auto-instantiated. This is probably the greatest source of bugs in Visual Basic programs, and it's the reason so many programmers code (and are taught to
code) things like this when closing down the program: For Each oThisForm In Forms If Not oThisForm Is Me Then Unload oThisForm
Next oThisForm Writing code like this is usually a direct response to an application that won't die, and it's a clear admission that we don't know what's going on. The correct
approach to this kind of problem is to identify which form isn't getting unloaded, and then to figure out why. You can monitor the Forms collection to help with this, but a very simple and effective way is to
instrument the Form_Load and Form_Unload events of every form you build. Here's the function I use to do it: Public Function FormSpy(
ByVal nOpen As Integer, _
ByVal sFormName As String) As Boolean
If nOpen = 0 Then
' You may actually need about ten beeps here to ' distinguish the Load and Unload sounds...
Beep Beep Debug.Print "<" & sFormName & " unloaded"
Else Beep Debug.Print ">" & sFormName & " loaded"
End If FormSpy = True
End Function This looks a bit peculiar, but here's how it is used: Private Sub Form_Load()
Debug.Assert FormSpy(1, TypeName(Me)) End Sub
Private Sub Form_Unload() Debug.Assert FormSpy(0, TypeName(Me)) End Sub
The only reason this is coded as a function is so it can be used in a Debug.Assert statement, which won't be compiled into the program. Adding this code to your programs can be very revealing,
as forms are loaded in all sorts of unexpected places. I've included a real example below to try it out on.
Example Code: A Classic Form Bug Here's a couple of
fragments from a class that's implemented as a form. It's coded as a form because we want to allow the user to edit some of its properties in text boxes (only one property, 'UserName', is shown). The code
shown here doesn't work: after calling the Edit method, the UserName property always returns "".Private m_sUserName As String
Public Property Get UserName() As String
UserName = m_sUserName End Property
Public Function Edit() As Boolean Me.Show vbModal If Me.ActiveControl Is cmdOk Then
m_sUserName = txtUserName.Text Edit = True Else
Edit = False EndIf End Function
Private Sub cmdOk_Click() Unload Me End Sub
Private Sub cmdCancel_Click() Unload Me End Sub When debugging this kind of problem it's very tempting to conclude that statements such as Unload don't work as advertised. However,
instrumenting this form's Form_Load and Form_Unload
events with the FormSpy function immediately reveals the problem: although the 'Unload Me' behind the OK button does its job, the fact of checking Me.ActiveControl
in the Edit method causes the visual component of the form to be re-instantiated. The original value of ActiveControl
is long gone, and its uninitialised value is anybody's guess. (What's worse is that if we designed the form so that ActiveControl just happens to be the OK button when the form loads, this code
will actually work and the bug will lay unseen until somebody changes the form later). To fix this we need to arrange for the form to stay loaded until after we've checked the value of ActiveControl
. Here are the relevant changes (shown in bold): Public Function Edit() As Boolean Me.Show vbModal If Me.ActiveControl Is cmdOk Then
m_sUserName = txtUserName.Text Edit = True Else
Edit = False EndIf Unload Me End Function
Private Sub cmdOk_Click() Me.Hide
End Sub
Private Sub cmdCancel_Click() Me.Hide End Sub One final note on this example. It's tempting to do away with the module-level variable m_sUserName and code
the Property Get like this: Public Property Get UserName() As String UserName = txtUserName.Text End Property This, of course, will also re-instantiate the form and return "".
|
We can draw a number of conclusions. Visual Basic's deference to novices can be crippling to intermediate-level programmers who are becoming more ambitious in their coding. In spite of this
there is nothing mysterious about forms and form behaviour, and perhaps we need to look more closely at how Visual Basic is taught. Finally, time spent devising helpful instrumentation pays big dividends by revealing
exactly what is going on in our programs. Only by knowing this can we hope to produce quality software. |