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

Visual Basic provides three kinds of code module: BAS files, FRM files and CLS files (leaving aside the essentially similar new module types introduced by VB5). Class modules offer an entirely new way of programming with VB, yet even those programmers who embrace classes usually don't go all the way. In particular, many miss the hidden class-like nature of the 'other' module types (BAS and FRM files), and so end up with a mish-mash of object-based and traditional VB code.

To experience the full power of object-based VB programming you have to jump in with both feet. Here I'll talk about some of the things I have found important for programming effectively in the object style with VB. I don't have enough space to teach you all about object-oriented programming, so I'll assume you have experimented with classes and try to provide information that will consolidate your own experience. My observations aren't comprehensive and they may not be 'the best way' of doing things. They are, however, based on real experience of Visual Basic programming rather than mere academic philosophising.

The first thing we need to know about a class is that it has a name. This name is as important as any other identifier, as we'll be using it to define variables (class instances) in our code. We need to decide on a convention for choosing your class names, and one that works very well is to use a noun prefixed by a capital C. For example, CTextFile would be a good name for a class that represents a text file. Traditional VB programmers are often tempted to use clsTextFile; personally I find this awkward and uneconomic, and it becomes even more awkward when deriving other identifiers from class names (for example, see the discussion of conversion functions in Chapter 3). As good a rationale as any is that the 'capital C' prefix is a de facto standard in object-oriented programming in other languages such as C++ and Java.

Once we have a class name we can define instances of our class. If you use type algebra to name your variables you'll need to choose a suitable prefix here too: I use 'o', hence Private oThisFile As CTextFile. I'll talk about global classes later, but for now that's all I'll say about ordinary class modules.

The deal with Form modules is a little bit more complicated. Many VB programmers still do not realise that forms are actually classes, and they continue to use the tired old VB3 idioms for manipulating forms. This even extends to using global variables or controls to communicate parameter information into and out of a form! The two most important things to understand about forms can be summarised as follows:

  • A Form module is really a Class module, and can be used in the same way. Specifically, we can define instances of a form, give it Public, Private and Friend (*) properties and methods, declare form instances WithEvents and use our form as an interface definition with Implements. There are some restrictions on what we can do with forms, but these are largely because a form also has a default Interface ('Form') as well as the one we create.
  • Whether or not we define any instances of our 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. This is possibly the biggest source of confusion I encounter in my dealings with other Visual Basic programmers.

(* Friend functions in forms are useless, as a form can't be exposed from an ActiveX component.)

My advice is, unless you have a good reason (see later for a semi-good reason), IGNORE Visual Basic's implicit form variables. Name your forms like classes (CLogin instead of frmLogin) and when you need an instance, create one with a variable definition. Here's an example. First the 'traditional' way:

    frmLogin.Show vbModal

and the way I'm suggesting:

    Dim oLogin As CLogin
    Set oLogin = New CLogin
    oLogin.Show vbModal

Doing it the first way breaks a fundamental rule of good program design: don't make an object any more visible than it has to be. Implicit form variables are global, which means that we can write code like sName = frmLogin!txtName.Text anywhere in the program, which is not conducive to modular design. As an aside, once you understand what's going on with forms you'll immediately realise why the popular idiom of coding Set frmLogin = Nothing in the Form_Unload event doesn't seem to work for any but implicitly-defined form variables.

Okay, so there may be a time when we definitely only want a single instance of our form, and we want to make it globally visible (for example, we want a form that encapsulates our program's error log browser). If, after all our careful design considerations this is really the way we want to go, there's nothing wrong with taking advantage of VB's 'magic form' feature. We need to consider what name we're going to give to the form, of course. We aren't going to use it as a class name, so we might consider naming it as an object variable (oErrorLog or goErrrLog instead of CErrorLog). Personally I like to emphasize that these are magic forms by naming them like classes but using F instead of C as the prefix (hence FErrorLog instead of CErrorLog).

Another idiom that works very well with forms is exemplified by the ShowMe method. It's conventional to show a form with code such as

    oLogin.Show vbModeless

Incidentally, another source of confusion is that this relies on an implicit Load of the form (a monumentally stupid feature of Visual Basic). Since invoking any property or method of a form loads the form first if it hasn't been loaded already, it's almost never necessary to code Load oLogin. This makes the code look a little tidier, so we'll go along with it (it's the other consequences of this feature that make it so devastating in practice.) It's worth spelling out the sequence of events here, since it's still not obvious:

  • We call oLogin.Show (it doesn't execute yet).
  • Because we called a method of the form, VB loads the form by executing an implicit Load oLogin.
  • The implicit Load fires the Form_Load event, so any code in this event executes before the form becomes visible. (If this is a magic form, the the Form_Initialize event also fires before the Form_Load, since VB's implicit form variables are auto-instantiated - ie. they're effectively defined with Dim oLogin As New CLogin rather than with Dim oLogin As CLogin: Set oLogin = New CLogin, which means that the Form_Initialize doesn't fire until the first reference. Another good reason not to use magic forms.)

Just to throw a spanner in the works, another common VB idiom is to code Me.Show at the start of the Form_Load event, to make the form appear before executing any time-consuming initialization code. Now things are very confusing because we've got two calls to Show! If you want to do this it's clearer to code Me.Visible = True in the Form_Load.

To get back to the plot, I suggest you NEVER use oLogin.Show to launch your forms. Far better to define a ShowMe method on the form itself:

Public Sub ShowMe()
    Me.Show vbModeless
End Sub

This doesn't look much different to the standard Show method, but consider our CLogin form in more detail. Instead of calling our method ShowMe we might call it GetUserInfo and code it like this:

Public Function GetUserInfo(ByRef sName As String, _
                            ByRef sPassword As String _
                            ) As vbMsgBoxReturn
    Me.Show vbModal
    If Me.ActiveControl Is cmdCancel Then
        GetUserInfo = vbCancel
    Else
        sName = txtName.Text
        sPassword = txtPassword.Text
        GetUserInfo = vbOk
    EndIf
    Unload Me
End Function

In use, we'd now use the login form like this:

Dim oLogin As CLogin, sUID As String, sPwd As String
Set oLogin = New CLogin
If oLogin.GetUserInfo(sUID, sPwd) = vbOk Then
    ' Use sUID and sPwd
Else
    ' whatever
Endif
Set oLogin = Nothing

This is far more natural way to program than the 'VB3' way (code inside frmLogin omitted):

Dim sUID As String, sPwd As String
frmLogin.Show vbModal
sUID = frmLogin!txtName.Text  ' This re-loads the form!
sPwd = frmLogin!txtPassword.Text
If sUID <> "" Then
    ' Use sUID and sPwd
Else
    ' whatever
EndIf
Unload frmLogin

Okay, that's forms. But is there any place for BAS files in this new world of object programming? Read on...

Many people are surprised to find that forms are really classes, but even more suprising is that BAS files also have some class-like features. If you drop a BAS module into your project and press F4, you'll get a property window with a single property in it - the module name. If this sounds familiar, that's because a class module also has this one property. In fact, in many ways BAS modules behave like single-instance auto-instantiated classes - or, to put it another way, rather like VB's magic forms. We can define Public or Private properties and methods in a BAS file, and we can use the module name to qualify references to such properties and methods.

So what use are these features? Just as with classes, we can use properties to define read-only, write-only or write-once variables, and we can qualify all references to global objects using the module name. For example, if we have a 'utilities' module we can invoke its functions (a.k.a. 'methods') like this:

    GUtil.UsefulThing

This is better than calling a global function (which is what this is) by simply using its name, since an unqualified reference gives no clue about where the definition belongs:

    UsefulThing

Note the naming convention I used for the module. Again, it's a class-like name, but this time I've used a G (for global) prefix in place of C or F. Unfortunately Visual Basic doesn't enforce the qualification of global properties and methods (unless we create more than one with the same name in different modules), but I suggest you ALWAYS qualify them because it makes for much clearer code. If you find yourself defining a class module and then creating a single instance that's available globally throughout the program (maybe an Error Handling object, for example), this is an ideal opportunity to use a BAS file instead.

Finally, we come full circle for a quick word about global classes. A global class (instancing property = GlobalMultiUse or GlobalSingleUse) behaves much like a BAS file, since it is auto-instantiated and we can access its properties and methods just as if they were global functions. This is both good and bad, for much the same reasons as a BAS file is: bad because calling a global function gives no clue about where the function lives or what other functions it's associated with, and good because it's handy for building simple function libraries that don't have any associated data (this is called 'statelessness' in OO jargon).

Oddly, to get back to the convenience of a BAS file we have to do more work with a gobal class. What we need is to be able to qualify our references to the class, even though we haven't defined an object. The actual object created from a global class is anonymous, but we can get around this by adding an extra property to the class. Here's an example, added to a hypothetical class GByteStuff (note the G prefix, as this is designed to be a global class), that implements a library of byte-oriented functions:

Public Property Get ByteStuff() As GByteStuff
    Set ByteStuff = Me
End Property

Unfortunately we can't use the exact name of the class for this property, so I've dropped the G. Now we can call functions in the GByteStuff library like this:

yByte = ByteStuff.LoByte(nNumber)

There's one last hack we need to do when working with global classes. GByteStuff comes from a real example where the class was part of a larger project that exported a number of ActiveX components. Making GByteStuff global was convenient, since it's a simple function library, but a global class only works as such outside of its native project. This means that to use GByteStuff inside the defining project we need to define an instance. The least painful way around this is to create a BAS module that defines an instance of each global class:

Public ByteStuff As New GByteStuff

That's all we need to do, because the 'As New' syntax will create the object the first time it is referenced. The syntax is now the same whether we're using GByteStuff functions within the defining project or in another project. (Obviously we're breaking the 'always qualify your BAS file accesses' rule here, but it's a special case!)
 

 
Key Spinner

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