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!)
|