cover
Copyright© 1998 by The Mandelbrot Set International, Ltd.


LOC Page

PUBLISHED BY
Microsoft Press
A Division of Microsoft Corporation
One Microsoft Way
Redmond, Washington 98052-6399

Copyright © 1998 by The Mandelbrot Set (International) Limited

All rights reserved. No part of the contents of this book may be reproduced or transmitted in any form or by any means without the written permission of the publisher.

Library of Congress Cataloging-in-Publication Data

Advanced Microsoft Visual Basic 6.0 / The Mandelbrot Set

       (International) Limited. -- 2nd ed.

              p.   cm.

        Includes index.

        ISBN 1-57231-893-7

        1. Microsoft Visual BASIC.   2. BASIC (Computer program language)

    I. Mandelbrot Set (International) Limited.  II. Title: Advanced

    Microsoft Visual Basic 5.

    QA76.73.B3A345  1998

    005.26'8--dc21                                                                        98-42530

                                                                                CIP

Printed and bound in the United States of America.

1 2 3 4 5 6 7 8 9 WCWC 3 2 1 0 9 8

Distributed in Canada by Penguin Books Canada Limited.

A CIP catalogue record for this book is available from the British Library.

Microsoft Press books are available through booksellers and distributors worldwide. For further information about international editions, contact your local Microsoft Corporation office or contact Microsoft Press International directly at fax (425) 936-7329. Visit our Web site at mspress.microsoft.com.

Intel is a registered trademark of Intel Corporation. ROOS, VBA2000, and Visual DateScope 2000 are trademarks of The Mandelbrot Set (International) Limited. ActiveMovie, ActiveX, Developer Studio, DirectShow, JScript, Microsoft, Microsoft Press, Visual Basic, Visual C++, Visual FoxPro, Visual InterDev, Visual J++, Visual SourceSafe, Visual Studio, Win32, Windows, and Windows NT are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries. Other product and company names mentioned herein may be the trademarks of their respective owners.

Acquisitions Editor: Stephen Guty

Project Editor: Wendy Zucker

Technical Editors: Marc Young, Jean Ross


Chapter 1 -- On Error GoTo Hell

 Chapter 1

On Error GoTo Hell

A Methodical Approach to Error Handling

PETER J. MORRIS

Peet is the Technical Director and a cofounder of The Mandelbrot Set (International) Limited (TMS). Peet, a former Microsoft employee, is acknowledged industry-wide as a Microsoft Windows and Visual Basic expert and is a frequent speaker at events such as VBITS and TechEd. As a developer and lecturer, he has taught Windows (SDK) API, Advanced Windows API, Visual Basic (all levels), OS/2 Presentation Manager, C, C++, Advanced C and C++, Pascal, compiler theory, OWL, Smalltalk, and CommonView.

Since the first edition of this book was released, this chapter has been tidied up a little. I've added some new rules and sidebars about handling errors in components and controls, as well as some examples of handling errors on a server.

What is an error? The short answer is, "Something that's expensive to fix." Dealing with errors is costly in terms of both money and time. As you probably know already, your test cycle will be longer, more complex, and less effective if you don't build appropriate error handling into your code right from the start. You should do all you can to reduce and handle errors in order to reduce costs, deliver quality code, and keep to schedules.

One way to eradicate errors—a way that I'll dismiss immediately—is to write error-free code. I don't think it's possible to write such pristine code. A more realistic way to deal with errors effectively is to plan for them properly so that when they do occur:

  • The application doesn't crash.

  • The error's root cause (and thus cure) is relatively easy to determine.

  • The error is as acceptable and as invisible to the user as is humanly possible.

So what must we do to put a good error handling scheme in place? It's a deceptively simple question with a big (subjective) set of answers. I think that acquiring and then using some fundamental knowledge is where we should start:

  • Ensure that all your developers truly understand how Visual Basic raises and then dispatches and handles errors.

  • Make sure that those same developers understand the consequences of writing code that is hard to debug and the true costs of any unhandled error.

  • Develop a suitable error handling strategy that's based on your understanding of the preceding two points and that takes into account your budget and line of business.

  • Apply your strategy; demand self-discipline and team discipline.

Handling errors properly in Visual Basic is also a good idea because of the alternative: Visual Basic's default error handling rules are rather severe. Unhandled errors are reported, and then an End statement is executed. Keep in mind that an End statement stops your application dead—no form QueryUnload or Unload events, no class Terminate events, not much of anything in fact.

To help you develop an effective strategy for dealing with errors, I'll go over some ideas that I consider vital to the process. These are presented (in no particular order) as a series of tips. "Pick 'n mix" those you think will suit you, your company, and of course your development project. Each tip is empirical, and we have employed them in the code we write at The Mandelbrot Set (International) Limited (TMS). I hope they serve you as well as they have served us!


Tip 1: Inconsistent as it is, try to mimic Visual Basic's own error handling scheme as much as possible.

When you call a Visual Basic routine that can fail, what is the standard way that the routine signals the failure to you? It probably won't be via a return value. If it were, procedures, for one, would have trouble signaling failure. Most (but not all) routines raise an error (an exception) when they want to signal failure. (This applies to procedures, functions, and methods.) For example, CreateObject raises an exception if it cannot create an object—for whatever reason; Open does the same if it cannot open a file for you. (Not all routines raise such exceptions. For example, the Choose function returns Null [thus, it requires a Variant to hold its return value just in case it ever fails] if you index into it incorrectly.) In other words, if a routine works correctly, this fact is signaled to the caller by the absence of any error condition.

Routines such as Open work like this primarily so that they can be used more flexibly. For example, by not handling the error internally, perhaps by prompting the user in some way, the caller is free to handle errors in the most suitable way. The caller can also use routines in ways perhaps not thought of by their writers. Listing 1-1 is an example using GetAttr to determine whether the machine has a particular drive. Because this routine raises exceptions, we can use it to determine whether or not a disk drive exists.

Listing 1-1 Using error handling to test for a disk drive

Public Function bDriveExists(ByVal sDriveAndFile As String) _
    As Boolean
' ===============================================================
'
' Module: modFileUtilities. Function: bDriveExists.
'
' Object: General
'
' Author - Peter J. Morris. TMS Ltd.
' Template fitted : Date - 01/01/97    Time - 00:00
'
' Function's Purpose/Description in Brief
'
' Determines whether the drive given in sDriveAndFile
' exists. Raises an exception if no drive string is given.
'
' Revision History:
'
' BY            WHY AND WHEN            AFFECTED
' Peter J. Morris. TMS Ltd. - Original Code 01/01/97, 00:00
'
' INPUTS - sDriveAndFile - holds the drive name, e.g., "C".
'          Later holds the name of the drive and the filename
'          on the drive to be created.
'
'
' OUTPUTS - Via return. Boolean. True if drive exists;
'           else False.
'
' MAY RAISE EXCEPTIONS
'
' NOTES: Uses formals as variables.
'        Uses delayed error handling.
'
' ===============================================================

    ' Set up general error handler.
    On Error GoTo Error_General_bDriveExists:

    Const sProcSig = MODULE_NAME & "General_bDriveExists"

    ' ========== Body Code Starts ==========
    ' These are usually a little more public - shown local
    ' for readability only.
    Dim lErr As Long
    Dim lErl As Long
    Dim sErr As String

    ' Constants placed here instead of in typelib for
    ' readability only.
    Const nPATH_NOT_FOUND       As Integer = 76
    Const nINTERNAL_ERROR_START As Integer = 1000
    Const nERROR_NO_DRIVE_CODE  As Integer = 1001

    ' Always default to failure.
    bDriveExists = False

    If sDriveAndFile <> "" Then

        ' "Trim" the drive name.
        sDriveAndFile = Left$(sDriveAndFile, 1)

        ' Root directory.
        sDriveAndFile = sDriveAndFile & ":\"

        ' Enter error-critical section - delay the handling
        ' of any possible resultant exception.
        On Error Resume Next

        Call VBA.FileSystem.GetAttr(sDriveAndFile)

        ' Preserve the error context. See notes later on
        ' subclassing VBA's error object and adding your own
        ' "push" and "pop" methods to do this.
        GoSub PreserveContext

        ' Exit error-critical section.
        On Error GoTo Error_General_bDriveExists:

        Select Case nErr

            Case nPATH_NOT_FOUND:
                bDriveExists = False

            ' Covers no error (error 0) and all other errors.
            ' As far as we're concerned, these aren't
            ' errors; e.g., "drive not ready" is OK.
            Case Else
                bDriveExists = True

        End Select

    Else

        ' No drive given, so flag error.
        Err.Raise nLoadErrorDescription(nERROR_NO_DRIVE_CODE)

    End If

    ' ========== Body Code Ends ==========

    Exit Function

' Error handler
Error_General_bDriveExists:

    ' Preserve the error context. See notes later on
    ' subclassing VBA's error object and adding your own "push"
    ' and "pop" methods to do this.
    GoSub PreserveContext

    ' **
    ' In error; roll back stuff in here.
    ' **

    ' **
    ' Log error.
    ' **

    ' Reraise as appropriate - handle internal errors
    ' further only.
    If (lErr < nINTERNAL_ERROR_START) Or _
       (lErr = nERROR_NO_DRIVE_CODE) Then

        VBA.Err.Raise lErr

    Else

        ' Ask the user what he or she wants to do with this
        ' error.
        Select Case MsgBox("Error in " & sProcSig & " " _
                           & CStr(lErr) & " " & _
                           CStr(lErl) & " " & sErr, _
                           vbAbortRetryIgnore + vbExclamation, _
                           sMsgBoxTitle)
            Case vbAbort
                Resume Exit_General_bDriveExists:

            Case vbRetry
                Resume

            Case vbIgnore
                Resume Next

            Case Else
                VBA.Interaction.MsgBox _
                                   "Unexpected error" _
                                   , vbOKOnly + vbCritical _
                                   , "Error"
                End

        End Select

    End If

Exit_General_bDriveExists:

    Exit Function

PreserveContext:

    lErr = VBA.Err.Number
    lErl = VBA.Erl
    sErr = VBA.Err.Description

    Return

End Function

Here are a few comments on this routine:

  • Although it's a fabricated example, I've tried to make sure that it works and is complete.

  • It handles errors.

  • It uses delayed error handling internally.

  • It's not right for you! You'll need to rework the code and the structure to suit your particular needs and philosophy.

  • The error handler might raise errors.

  • It doesn't handle errors occurring in the error handler.

  • It uses a local subroutine, PreserveContext. This subroutine is called only from within this routine, so we use a GoSub to create it. The result is that PreserveContext is truly private and fast—and it doesn't pollute the global name space. (This routine preserves the key values found in the error object. Tip 11 explains a way to do this using a replacement Err object.)

Within bDriveExists, I've chosen to flag parameter errors and send the information back to the caller by using exceptions. The actual exception is raised using the Raise method of the Visual Basic error object (Err.Raise) and the return value of a function (nLoadErrorDescription). This return value is used to load the correct error string (typically held in string resources and not a database since you want to always be able to get hold of the string quickly). This string is placed into Err.Description just before the Raise method is applied to the error object. Reraising, without reporting, errors like this allows you to build a transaction model of error handling into your code. (See Tip 14 for more on this topic.)

The nLoadErrorDescription function is typically passed the error number (a constant telling it what string to load), and it returns this same number to the caller upon completion. In other words, the function could look something like this (omitting any boilerplate code):

Public Function nLoadErrorDescription(ByVal nCode As Integer)

    ' Return the same error code we're passed.
    nLoadErrorDescription = nCode

    ' Load the error text for nCode from some source and assign it
    ' to Err.Description.
    Err.Description = LoadResString(nCode)

    Exit Function

End Function

In this example, we're using a string resource to hold the error text. In reality, the routine we normally use to retrieve an error string (and, indeed, to resolve the constant) is contained in what we call a ROOS—that's a Resource Only OLE Server, which we'll come back to in Tip 10.

A good error handler is often complex, question: What will happen if we get an error in the error handler? Well, if we're in the same local scope as the original error, the error is passed back up the call chain to the next available error handler. (See Tip 5 for more information on the call chain and this mechanism.) In other words, if you're in the routine proper when this second error occurs, it will be handled "above" your routine; if that's Visual Basic, you're dead! "OK," you say, "to handle it more locally, I must have an error handler within my error handler." Sounds good—trouble is, it doesn't work as you might expect. Sure, you can have an On Error Goto xyz (or On Error Resume Next or On Error Resume 0) in your error handler, but the trap will not be set; your code will not jump to xyz if an error occurs in your error handler. The way to handle an error in your error handler is to do it in another procedure. If you call another procedure from your error handler, that routine can have an error trap set. The net effect is that you can have error handling in your error handler just as long as another routine handles the error. The ability to handle errors in error handlers is fundamental to applying a transaction processing model of error handling to your application, a subject I'll explain further in Tip 14.

To recap, the reason GetAttr doesn't handle many (if any) internal errors is that to do so would take away its very flexibility. If the routine "told" you that the drive didn't exist, by using, say, a message box, you couldn't use it the way we did in bDriveExists.

If you're still not convinced, I'll be saying a little more on why raising errors is better than returning True or False later. But for now, let's think BASICA!


Tip 2: Use line numbers in your source code.

Line numbers!? Yup, just like those used in "real" Basic. Bear with me here—I'll convince you!

In older versions of Basic, line numbers were mandatory and often used as "jump targets." A jump target is a line number used with a GoTo, such as GoTo 2000. The 2000 identifies the start of a block of code to execute next. After GoTo came GoSub (and Return). Now you had a "Basic subroutine," albeit one with a strange name: GoSub 2000. You can think of the (line) number almost as an address (just as in C). These days, of course, Basic is Visual Basic and we use symbolic names for labeling such jump targets (real subroutines, just like those in C and other programming languages). Line numbers have become a peculiarity designed to allow nothing more than some level of backward compatibility with some other version of Basic.

Or then again, maybe not. In Visual Basic, Erl, a Visual Basic (undocumented in Visual Basic 4, 5, and 6 but present in all versions of Visual Basic thus far) "global variable," gives you access to the line number of any erroring line of code. So by using line numbers and by using Erl in your error handlers, you can determine which line of code erred—wow! What happens to Erl if you don't use line numbers? Easy—it will always be 0.

Of course, you won't want to start typing line numbers in by hand. You need some automation. At TMS, we add line numbers to our code using an internal tool we originally developed for working with Visual Basic 2. It now works as an add-in under Visual Basic 6. There are tools on the market that can do the same for your code.

After the first edition of our book came out, we received lots of mail asking us where such a line numbering tool could be obtained. The demand was so great, and the available tools were so few, that we put an FOC line number wizard on our web site (www.themandelbrotset.com/html/downloads.html). That tool is still there, so please feel free to download a copy of it.

At TMS, we don't work with line numbers in our source code, however. We add them only when we're doing a ship build—that is, when we want to ship a binary to, say, beta testers or to manufacturing for an impending release. We use our internal tool to build a new version of the code, complete with line numbers, and then we make an executable from that. We store the line numbered source code in our source control system and ship the executable. We cross-reference the EXE version number (the Auto Increment option is just great here) to the source code stored in the source control system. Every time we do a new build for shipping, we create a new subproject whose name is the version number of the build and then store the line numbered source code in it along with a copy of the binary image. If an error report comes in, we can easily refer back to the source code to find the erroring line (very easy if you're using Microsoft Visual SourceSafe). Typically, the error report will contain details of the module, routine, and line number of the error.

Listing 1-2 is a typical Click event, line numbers and all.

Listing 1-2 Generic Click event with line numbers

Private Sub Command1_Click()
' =============================================================
' Module Type : Form
' Module Name : Form1
' Object      : Command1
' Proc Type   : Sub
' Proc Name   : Click
' Scope       : Private
' Author      :
' Date        : 01/01/97 00:00
'
' History     : 01/01/97 00:00: Peter J. Morris : Original Code.
' =============================================================

' Set up general error handler.
On Error GoTo Error_In_Command1_Click:

1  Dim sErrorDescription As String

2  Const sProcSig = MODULE_NAME & "Command1_Click"

       ' ========== Body Code Starts ==========

3  Debug.Print bDriveExists("")

       ' ========== Body Code Ends ==========

4  Exit Sub

' Error handler
Error_In_Command1_Click:

5  With Err
6      sErrorDescription = "Error '" & .Number & " " & _
       .Description & "' occurred in " & sProcSig & _
       IIf(Erl <> 0, " at line " & CStr(Erl) & ".", ".")
7  End With

8  Select Case MsgBox(sErrorDescription, _
                      vbAbortRetryIgnore, _
                      App.Title & " Error")

       Case vbAbort
9      Resume Exit_Command1_Click:
10     Case vbRetry
11         Resume
12     Case vbIgnore
13         Resume Next
14     Case Else
15         End

16  End Select

Exit_Command1_Click:

End Sub

Notice in Listing 1-2 that sProcSig is made up of the module name (Form1) and the routine name (Command1_Click). Notice also that the error handler examines Erl to "see" whether line numbers have been used. Figure 1-1 shows what's typically displayed when an error occurs using this kind of scheme.

Figure 1-1 Error and line number information

Of course, the actual code that makes up the error handler is entirely up to you. If you use this scheme, I recommend you have a module-level constant to hold your module name and use a routine-level constant to hold the routine name plus the module name:

Module Declaration Section

Private Const MODULE_NAME As String = "Form1."

Command1_Click Event

Const sProcSig As String = MODULE_NAME & "Command1_Click"


Tip 3: Raise exceptions when possible because return values will be ignored.

This tip supplements Tip 1: "Inconsistent as it is, try to mimic Visual Basic's own error handling scheme as much as possible." Since Visual Basic 4, a function can be called like a subroutine. (In Visual Basic 3 and earlier, it couldn't.) To demonstrate this, consider the following code fragments:

Sub Command1_Click ()

    Debug.Print SomeFunc()
    Call SomeFunc

End Sub

Function SomeFunc () As Integer

    SomeFunc = 42

End Function

The line Call SomeFunc is illegal in Visual Basic 3 but legal in Visual Basic 4 and later. (It's VBA!) In case you're wondering why this is so, the facility was added to VBA (Visual Basic for Applications) to allow you to write routines that were more consistent with some of Visual Basic's own routines, such as MsgBox, which acts sometimes like a function and sometimes like a statement (or a C type procedure if you're used to that language). (In Tip 4, you'll find out how to write your own MsgBox routine.)

A side effect of all this is that routines that return some indication of success or failure might now have that result ignored. As C and SDK programmers know only too well, this will cause problems! In Visual Basic 3, the programmer always had to use the return value. Typically, he or she would use it correctly. If a programmer can ignore a routine's returned value (say it's not a database handle but a True/False value—that is, either it worked or it failed), however, he or she usually will ignore it.

Exceptions, on the other hand, cannot easily be ignored (except by using On Error Resume Next or On Error Resume 0—both easy to test for and legislate against). Also, keep in mind that "newer" Visual Basic developers sometimes lack the necessary self-discipline to use and test return values correctly. By raising exceptions, you force them to test and then to take some appropriate action in one place: the error handler.

Another reason to use exceptions is that not using them can cause your code to become more difficult to follow—all those (un)necessary conditional tests to see that things have worked correctly. This kind of scheme, in which you try some code and determine that it didn't work by catching a thrown exception, is pretty close to "structured exception handling" as used in C++ and Microsoft Windows NT. For more on structured exception handling, see the MSDN Library Help. (Select the Contents tab, and follow this path: Visual C++ Documentation; Reference; C/C++ Language and C++ Libraries; C++ Language Reference; Statements; Exception Handling; Structured Exception Handling.)

Here's an example of a structured exception handling type of scheme:

Private Sub SomeWhere()

    If a() Then
        .
        .
        .
        If b() Then
            .
            .
            .
            If c() Then
                    .
                    .
                    .
            End If
        End If
    End If

End Sub

This example is not too hard to figure out. But I'm sure you've seen far more complex examples of nesting conditionals, and you get the idea! Here's the same code using exceptions to signal errors in a, b, or c:

Private Sub SomeWhere()

' TRY
On Error Goto ????

    a()
        .
        .
        .
    b()
        .
        .
        .

    c()
        .
        .
        .

' CATCH
????

    ' Handle exception here.

End Sub

Can you see the flow any easier here? What's implied by the presence of the error handler is that to get to the call to b, a must function correctly. By losing the If, you're losing some plain readability, but you're also gaining some readability—the code is certainly less cluttered. Of course, sometimes code is clear just because you're used to it. Consider replacing b, for instance, with a call to Open. If you were to use the If…Then scheme to check for errors, you couldn't check for any errors in Open because you can't put conditional statements around a procedure. So it's easy for you to accept the fact that after Open is called, if an error occurs, the statement following Open will not run. It works the same with the b function. If an error occurs in the b function, the error routine rather than the statement that follows b will execute.

If you adopt this kind of error handling scheme, just make sure that you have projectwide collaboration on error codes and meanings. And by the way, if the functions a, b, and c already exist (as used previously with the If statements), we'll be using this "new" ability to ignore returned values to our advantage.

NOTE

Once again, if a routine's returned value can be ignored, a programmer will probably ignore it!


Tip 4: Automatically log critical MsgBox errors.

One way to log critical MsgBox errors is by not using the standard message box provided by VBA's Interaction.MsgBox routine. When you refer to an object or a property in code, Visual Basic searches each object library you reference to resolve it. Object library references are set up in Visual Basic's References dialog box. (Open the References dialog box by selecting References from the Project menu.) The up arrow and down arrow buttons in the dialog box move references up and down in a list so that they can be arranged by priority. If two items in the list use the same name for an object, Visual Basic uses the definition provided by the item listed higher in the Available References list box. The three topmost references (Visual Basic For Applications, Visual Basic Runtime Objects And Procedures, and Visual Basic Objects And Procedures) cannot be demoted (or shuffled about). The caveat to all this prioritizing works in our favor—internal modules are always searched first.

Visual Basic 6 allows you to subclass its internal routines such as MsgBox and replace them with your own (through aggregation). Recall that in the code shown earlier (in Listing 1-1) some of the calls to MsgBox were prefixed with VBA. This explicitly scopes the call to VBA's MsgBox method via the Visual Basic For Applications type library reference. However, calls to plain old MsgBox go straight to our own internal message box routine.

A typical call to our new message box might look like this:

MsgBox "Error text in here", _
    vbYesNo + vbHelpButton + vbCritical, sMsgBoxTitle

The vbHelpButton flag is not a standard Visual Basic constant but rather an internal constant. It's used to indicate to MsgBox that it should add a Help button. Also, by adding vbCritical, we're saying that this message (error) is extremely serious. MsgBox will now log this error to a log file.

To replace MsgBox, all you have to do is write a function (an application method really) named MsgBox and give it the following signature. (The real MsgBox method has more arguments that you might also want to add to your replacement; use the Object Browser to explore the real method further.)

Public Function MsgBox _
( _
    ByVal isText As String _
    , Optional ByVal inButtons As Integer _
    , Optional ByVal isTitle As String  _
)

Here's an example of a trivial implementation:

Public Function MsgBox _
( _
    ByVal isText As String _
    , Optional ByVal inButtons As Integer _
    , Optional ByVal isTitle As String _
)

    Dim nResult As Integer

    nResult = VBA.Interaction.MsgBox(isText, inButtons, isTitle)

    MsgBox = nResult

End Function

Here we're logging (implied by the call to LogError) the main message text of a message box that contains the vbCritical button style. Notice that we're using the VBA implementation of MsgBox to produce the real message box on screen. (You could use just VBA.MsgBox here, but we prefer VBA.Interaction.MsgBox for clarity.) Within your code, you use MsgBox just as you always have. Notice also that in our call to LogError we're logging away the user's response (nResult) too—"I'm sure I said 'Cancel'!"

Another good idea with any message box is always to display the application's version number in its title; that is, modify the code above to look like this:

sTitle = App.EXEName & "(" & App.Major & "." & _
                             App.Minor & "." & _
                             App.Revision & ")"

nResult = VBA.Interaction.MsgBox(isText, inButtons, _
                                 sTitle & isTitle)

Figure 1-2 shows the message box that results from this code.

Figure 1-2 Using your version number in message boxes

Of course, you don't have to use VBA's MsgBox method to produce the message box. You could create your own message box, using, say, a form. We create our own custom message boxes because we often want more control over the appearance and functionality of the message box. For example, we often use extra buttons (such as a Help button, which is what the vbHelpButton constant was all about) in our message boxes.

One nifty way to log error events (or any other event you might consider useful) is to use the App object's LogEvent method, which logs an event to the application's log target. On Windows NT platforms, the log target is the NT Event Log; on Windows 9x machines, the log target writes to a file specified in the App.LogPath property. By default, if no file is specified, events are written to a file named VBEVENTS.LOG.

This code

Call App.LogEvent("PeetM", vbLogEventTypeInformation)
Call App.LogEvent(Time$, vbLogEventTypeError)

produces this output in the log:

Information Application C:\WINDOWS\vbevents.log: Thread ID:
    -1902549 ,Logged: PeetM
Error Application C:\WINDOWS\vbevents.log: Thread ID:
    -1902449 ,Logged: 15:11:32

Interestingly, App.LogPath and App.LogMode are not available at design time and are available as read-only at run time, so how do you set them? You set them with App.StartLogging. A disadvantage to these routines is that App.LogEvent is available only in a built executable—not very useful for debugging in the Integrated Development Environment (IDE)! Now the good news: you can improve on this behavior by using the Win32 API directly from your application to log events to the NT Event Log (under Windows NT) or to a file (under Windows 9x). If you're going to log events this way I would suggest that you do so by ignoring the advice given in the Support Online article, HOWTO: Write to the NT Event Log from Visual Basic. (You can find this article by searching for article ID Q154576 on Microsoft's web site, www.microsoft.com.) Instead, wrap the necessary API calls (steal the code required from the HOWTO article) within a replacement App object that is contained within an ActiveX DLL (to which you have a reference in your project). This means that you'll still use App.LogEvent and the other routines, but instead of calling into the "real" App object, you're calling into the one you've provided in the DLL (which is compiled, of course). You can write this DLL so that you can easily change App.LogFile or any other routine (if you're running Windows 9x).


Tip 5: Have an error handler in every routine.

Because Visual Basic nests routines into local address space, all errors happen locally. An unhandled error in some routine that might be handled above that routine, in another error handler, should be considered unhandled because it will probably destabilize the application.

Let's go over that again, but more slowly. Visual Basic handles local errors. By this, I mean that whenever an error handler is called it always thinks it's acting upon an error produced locally within the routine the error handler is in. (Indeed, a bit of obscure syntax, normally unused because it's implied, On Local Error GoTo, gives this little secret away.) So if we write some functions named SubA, SubB, and SubC and arrange for SubA to call SubB and SubB in turn to call SubC, we can spot the potential problem. (See Figure 1-3.) If SubC generates an error, who handles it? Well, it all depends. If we don't handle it, Visual Basic will. Visual Basic looks up Err.Number in its list of error strings, produces a message box with the string, and then executes an End for you. If, as in Figure 1-3, SubA handles errors, Visual Basic will search up through the call chain until it finds SubA (and its error handler) and use that error handler instead of its own default error handler. Our error handler in SubA, however, now thinks that the error happened locally to it; that is, any Resume clause we might ultimately execute in the error handler works entirely within the local SubA routine.

Figure 1-3 The call chain in action

Your code always runs in the context of some event handler; that is, any entry point into your code must ultimately be in the form of an event handler. So substituting SubA with, say, Form_Load, you could now write a catchall error handler by providing an error handler in Form_Load. Now, when SubC generates its error (I'm assuming here that these functions are only ever called from Form_Load), Visual Basic will find the local error handler in Form_Load and execute it. Ultimately, this error handler will execute a Resume statement. For argument's sake, let's say that it's Resume Next.

The Next here means after the call to SubB. OK, so what's the problem? If a problem exists, it's buried inside SubB and SubC—we don't know what they did! Imagine this scenario. Maybe SubC opened some files or perhaps a database or two, and somewhere within SubC, it was also going to close them. What happens if the erroring code happened somewhere in between these two operations—say, the files or databases got opened but were never closed? Again it depends, but loosely speaking, it means trouble.

NOTE

The situation described above could be worse, however. Maybe instead of Resume Next we simply used Resume, that is, try again. This will result in an attempt to open the same files again; and as we all know, this attempt may fail for many reasons—perhaps instead of using FreeFile, you used hard-coded file handle IDs, or maybe you opened the files last time with exclusive access.

Unfortunately, when Visual Basic executes an error handler, there's no easy way of telling whether the error handler was really local to the error. So there's no way to guarantee that you handled it properly. And of course, there's no way to install a global error handler that's called automatically by Visual Basic whenever an error occurs. There's no way around it: to write professional and robust applications, we must have error handlers absolutely everywhere!


Tip 6: Write meaningful error logs (to a central location if possible).

By way of an example, Listing 1-3 is a typical log file entry produced by our internal application template code. No explanation is provided because most of the entry is pretty obvious.

Listing 1-3 Typical log file entry

******************************************************************
* Error Entry Start. TEST. Created 21 March 1998 19:01
******************************************************************
The Application:
----------------

C:\TMS\TEMPLATE\TEST.EXE  Version 1.0.15
OS App Name C:\TMS\TEMPLATE\TEST.EXE

The Error:
----------
An error has occurred in C:\TMS\TEMPLATE\TEST.EXE - the TMS error code associated
with this error is 000053. If the problem persists, please report the error to TMS
support. The error occurred at line 100.

The error probably occurred in frmMainForm.cmd1_Click.
The standard VB error text for this error is 'File not found'.

Active Environment Variables:
-----------------------------
TMP=C:\WINDOWS\TEMP
winbootdir=C:\WINDOWS
COMSPEC=C:\COMMAND.COM
PATH=C:\WINDOWS;C:\WINDOWS\COMMAND;C:\;C:\DOS
TEMP=C:\TEMP
DIRCMD=/OGN/L
PROMPT=$e[0m[$e[1;33m$p$e[0m]$_$g
CMDLINE=WIN
windir=C:\WINDOWS

Relevant Directories:           Attr:
---------------------
Windows DIR C:\WINDOWS        - 16
System  DIR C:\WINDOWS\SYSTEM - 16
Current DIR C:\TMS\TEMPLATE   - 16
Versions:
----------
Windows    - 3.95
DOS        - 7.0
Mode       - Enhanced
CPU        - 486 or Better
COPRO      - True
Windows 95 _ True (4.03.1214)B

Resources:
----------
Free Mem (Rough) 15,752 MB
Free GDI  (%) 79
Free USER (%) 69
Free Handles 103

Other:
------
Threads           - 64
VMs              - 4
Registered Owner _ Peter J. Morris

******************************************************************************
* Error Entry End. TEST
******************************************************************************

******************************************************************************
* Stack Dump Start. TEST. Created 21 March 1998 19:01
******************************************************************************
Stack Frame: 001 of 003 AppObject   - Run         Called @ line 70 CheckStack
Stack Frame: 002 of 003 AppObject   - CheckStack  Called @ line 10 cmd1_Click
Stack Frame: 003 of 003 frmMainForm - cmd1_Click
******************************************************************************
* Stack Dump End. TEST
******************************************************************************

******************************************************************************
* DAO Errors Start. TEST. Created 21 March 1998 19:01
******************************************************************************
No Errors
******************************************************************************
* DAO Errors End. TEST
******************************************************************************

Also, see the note on stack frames later in this chapter for a fuller explanation of the Stack log entries.

The log files you write can be centralized; that is, all your applications can write to a single file or perhaps to many different files held in a central location. That "file" could be a Microsoft Jet database. Now if you log meaningful information of the same kind from different sources to a database, what have you got? Useful data, that's what! At TMS, we created a system like this once for a client. All the data gathered was analyzed in real time by another Visual Basic application and displayed on a machine in the company's support department. The application had some standard queries it could throw at the data (How's application xyz performing today?), as well as a query editor that the company could use to build its own queries on the data. (Show me all the Automation errors that occurred for user abc this year, and sort them by error code.) All the results could be graphed, too—an ability that, as is usual, allows the true nature of the data statistics to become apparent.

After a little while, it wasn't just Support that accessed this database. User education used it to spot users who were experiencing errors because of a lack of training, and developers used it to check on how their beta release was running. Remember, users are generally bad at reporting errors. Most prefer to Ctrl+Alt+Delete and try again before contacting support. By logging errors automatically, you don't need the user to report the error (sometimes incorrectly or with missing information: "Let's see, it said something about…"); it's always done by the application, and all the necessary information is logged automatically.

Figure 1-4 shows an example of the kind of output that's easy to produce from the log:

Click to view at full size.

Figure 1-4 Graphing error log data

This chart shows how many users have produced trappable errors in various interesting applications.

Whenever we hit an error, we determine whether we're running in Visual Basic's IDE and then log or output different error text and do some other stuff differently depending on the result of that test. The reason we do this is that programmers are not end users, meaning that a programmer doesn't mind seeing "Input past end of file," but users almost always mind! If you know which context you're running in, you can easily switch messages.

The test we do internally at TMS to determine whether we're running in the IDE involves a routine called InDesign. Here's the code (the explanation follows):

Public Function InDesign() As Boolean

    ' ****************************************
    ' The only thing Debug.Assert is good for!
    ' ****************************************

    Static nCallCount   As Integer
    Static bRet         As Boolean  ' By default this is set to False.

    nCallCount = nCallCount + 1

    Select Case nCallCount

        Case 1: ' First time in
            Debug.Assert InDesign()

        Case 2: ' Second time in so we must have executed Debug.Assert...
            bRet = True

    End Select

    ' If Debug.Assert called, we need to return True to prevent the trap.
    InDesign = bRet

    ' Reset for future calls.
    nCallCount = 0

End Function

In the earlier versions of Visual Basic (previous to Visual Basic 6), InDesign used API calls to determine whether the Visual Basic IDE was kicking around. The version of InDesign (not shown here) in the first edition of our book evolved from some Visual Basic 4 code and therefore needed to cater to both the 16-bit and 32-bit worlds. We modified this code for the pure 32-bit world and replaced it with what amounts to a call to GetModuleHandle:

Private Function InDesign() As Boolean

    InDesign = 0 < GetModuleHandle("VBA5.DLL")

End Function

The only problem with this code was that you needed to know the name of the DLL that implements the IDE, which in this case was VBA5.DLL. Who knew that this would be VBA6.DLL for version 6 and who knows what it will be for version 7 and so on? By the way, this code works because if the application is running under the IDE in Win32, the IDE (and its DLLs and so on) must be loaded into the same process space as the application. The DLLs of other processes cannot be seen (easily, anyway); ergo, if you can see it you must have it loaded, and if you have it loaded you must be running in the IDE.

Anyway, back to the InDesign code shown earlier. This "new" cut of the code should work for all future versions of Visual Basic (as well as for version 6). This code essentially uses the fact that Debug.Assert is coded in such a way as to make it suboptimal (an explanation of this statement follows shortly). Because the Debug object effectively goes away when you make an EXE, it follows that methods applied to it, like Print, also have no effect—in fact, they don't even run. Because the Assert method is such a method, we can make use of this disappearing act to determine whether the code is running in design mode.

The first time we call the function, which requires only a simple If InDesign Then statement, nCallCount is zero and bRet is False (initialized by default). Notice that both variables are declared as static, meaning they are accessed locally but stored globally. In other words, they are shared, persistent objects that can be accessed only within the scope of the subroutine in which they're declared. We increment nCallCount and then execute the Select Case statement. Obviously nCallCount is now 1, so the Case 1 code executes. At this point, if we're running in design mode, the Debug.Assert line causes us to reenter the routine. This time, nCallCount = nCallCount + 1 increments the static nCallCount to 2, and the Case 2 code sets bRet to True. Note that True is returned to the call made to Debug.Assert from the first entry into InDesign. Because we've asserted something that's True, we don't execute a Stop here. Instead, we return to the line of code to be executed after the call to Debug.Assert, which is the InDesign = bRet code (again). Once more we return True (because bRet is still set to True from the previous call to InDesign). This final value of True is now returned to the original caller to indicate, "Yes, we are running in design mode."

Now consider what happens if we're running as an EXE. Basically this means that the line Debug.Assert InDesign is missing from the routine. In this case, the only call made to our routine returns the state of bRet—False by default. If you're worried about the clock cycles taken here (that as an EXE we increment an integer and then set it to zero again), don't be—it's fast! If you insist, however, you can wrap the routine so that it's called just once, perhaps upon application start-up.

OK, why did I say that Debug.Assert was suboptimal? Normally assertions are used to implement what is known in the trade as a "soft if." Consider this code:

nFile = FreeFile

If FreeFile fails, what does nFile equal? Actually, like Open, FreeFile raises an exception to indicate failure (maybe it knows that return values can, and will, be ignored), but for the sake of argument let's say FreeFile returns 0. To detect this, as you should if you're building really critical applications that must cope with and recover from every possibility, expand the code to this:

nFile = FreeFile

If nFile <> 0 Then
    .
    .
    .
End If

Adding the conditional test and the indentation complicates the code. The execution time increases due to the branch, the evaluation of both the expressions on either side of the angle brackets, and of course the comparison itself. For all we know, we may never need this code—after all, what is the probability that FreeFile will fail in real life? To test this, sanitize the code, and make it more efficient, we would use a "soft if" conditional instead of a "hard if" conditional:

nFile = FreeFile

Assert nFile <> 0

.
.
.

Here we're asserting our belief that FreeFile will never return 0. (Note that we've lost the indentation.) Now we build the application and send it out to test. If the assertion fails, the probability that we've run out of file handles surely approaches 1, whereas if it doesn't fail, the probability approaches 0. In this case, we can decide that the likelihood of failure is so remote that we can effectively ignore it. If the assertion never fails, we use conditional compilation to remove it and build the final EXE. In fact, we'd normally remove all assertions either by turning them into "hard ifs" or by removing them altogether. Never ship with assertions in your code. By the way, all of the previous was C-speak (for example, I'd do it this way in C or C++), and therein lies the rub. In Visual Basic you can't do otherwise because Debug.Assert is removed for you whenever you build an EXE. "Great," you say. "So now I must never ship with assertions in my code?" (I just said this practice was a good one, but only when you ship the final EXE.) "How do I determine if an assertion failed during a test if it's not even there?" Aha—the plot thickens. Assertions in Visual Basic seem to be there solely for the developer and not the tester, meaning they work only in the IDE environment. In other words, suboptimal. That is, of course, unless you ship the source and IDE when you go out to beta!

Back to the story. By using InDesign we can, as mentioned earlier, do things a little differently at run time depending upon whether we're running in the IDE. We at TMS usually store the result of a single call to InDesign in a property of the App object called InDesign. (We replace the real App object with our own—also called App—and set this property at application start-up.)

Another use of App.InDesign is to turn off your own error handling altogether. Now I know that Visual Basic allows you to Break On All Errors, but that's rarely useful, especially if you implement delayed error handling. Instead, use App.InDesign to conditionally turn error handling on or off:

If Not App.InDesign Then On Error GoTo ...

The reason for this is that one of the last things you want within the IDE is active error handlers. Imagine you're hitting F8 and tracing through your code. I'm sure you know what happens next—you suddenly find yourself in an error handler. What you really want is for Visual Basic to issue a Stop for you on the erroring line (which it will do by default if you're using the IDE and hit an error and don't have an error handler active). The code above causes that to happen even when your error handling code has been added. Only if you're running as an EXE will the error trap become enabled.


Tip 7: Use assertions.

I've already briefed you on some stuff about assertions; here's the full scoop.

Assertions are routines (in Visual Basic) that use expressions to assert that something is or is not True. For example, you might have a line of code like this in your project:

    nFile = FreeFile

So how do you know if it works? Maybe you think that it raises an exception if all your file handles are taken up. (The Help file doesn't tell you.) We wouldn't leave this to chance. What we'd do during both unit and system testing is use assertions to check our assumption that all is indeed well. We would have a line that looks like this following the line above:

    Call Assert(nFile <> 0, "FreeFile")

This checks that nFile is not set to 0. Assertions are easy to use and extremely handy. They would be even better if Visual Basic had a "stringizing" preprocessor like the one that comes with most C compilers. Then it could fill in the second parameter for you with the asserted expression, like this:

    Call Assert(nFile <> 0, "nFile <> 0")

Assertions should be removed at run time. They serve only for testing during development, a kind of soft error handler, if you will. (This removal could be done using the App.InDesign property described earlier.) If an assertion regularly fails during development, we usually place a real test around it; that is, we test for it specifically in code. For the preceding example, we would replace

    Call Assert(nFile <> 0, "FreeFile")

with

If nFile = 0 Then
    Err.Raise ????
End If

If an assertion doesn't fail regularly (or at all) during development, we remove the assertion.

If you're asking yourself, "Why isn't he using Debug.Assert?" you need to go back and read all of Tip 6.


Tip 8: Don't retrofit blind error handlers.

The best error handlers are written when the routine they protect is being written. Tools that insert error handlers for you help but are not the answer. These tools can be used to retrofit semi-intelligent error handlers into your code once you're through writing—but is this a good idea? Your application will be error handler-enabled, that's for sure; but how dynamic will it be in its handling of any errors? Not very!

We rarely use any kind of tool for this purpose because in fitting a blind error handler there is little chance of adding any code that could recover from a given error situation. In other words, by fitting an error handler after the fact, you might just as well put this line of pseudocode in each routine:

On Error Condition Report Error

You're handling errors but in a blind, automated fashion. No recovery is possible here. In a nutshell, a blind error handler is potentially of little real use, although it is of course better than having no error handling at all. Think "exception" as you write the code and use automation tools only to provide a template from which to work.


Tip 9: Trace the stack.

As you saw in the log file in Listing 1-3, we dump the VBA call stack when we hit an unexpected error because it can be useful for working out later what went wrong and why. We build an internal representation of VBA's stack (because VBA's stack is not actually available—shame), using two fundamental routines: TrTraceIn and TrTraceOut. Here they are in a typical routine:

Public Sub Testing()

' Set up general error handler.
On Error GoTo Error_General_Testing:
    Const sProcSig = MODULE & " General.Testing"
    Call TrTraceIn(sProcSig)

    ' ========== Body Code Starts ==========
    .
    .
    .

    ' ========== Body Code Ends ==========

    Call TrTraceOut(sProcSig)
    Exit Sub

' Error handler.
Error_General_Testing:
    .
    .
    .

End Sub

These routines are inserted by hand or by using the same internal tool I mentioned earlier in Tip 2 that adds line numbers to code. Notice that sProcSig is being passed into these routines so that the stack can be built containing the name of the module and the routine.

The stack frame object we use internally (not shown here) uses a Visual Basic collection with a class wrapper for its implementation. The class name we use is CStackFrame. As a prefix, C means class, and its single instance is named oStackFrame. We drop the o prefix if we're replacing a standard class such as Err or App.


Tip 10: Use a ROOS (Resource Only OLE Server).

A basic ROOS (pronounced "ruse") is a little like a string table resource except that it runs in-process or out-of-process as an Automation server. A ROOS provides a structured interface to a set of objects and properties that enables us to build more flexible error handling routines.

For example, the ROOS holds a project's error constants (or rather the values mapped to the symbols used in the code that are resolved from the object's type library). The ROOS also holds a set of string resources that hold the actual error text for a given error and the methods used to load and process errors at run time. To change the language used in error reports or perhaps the vocabulary being used (for example, user vs. programmer), simply use a different ROOS. (No more DLLs with weird names!)


Tip 11: Replace useful intrinsic objects with your own.

Our main ROOS contains a set of alternative standard object classes, TMSErr and TMSApp, for example. These are instantiated as Err and App at application start-up as part of our application template initialization. (All our Visual Basic applications are built on this template.) By creating objects like this, we can add methods, properties, and so on to what looks like one of Visual Basic's own objects.

For example, our error object has extra methods named Push and Pop. These, mostly for historical reasons, are really useful methods because it's not clear in Visual Basic when Err.Clear is actually applied to the Err object—that is, when the outstanding error, which you've been called to handle, is automatically cleared. This can easily result in the reporting of error 0. Watch out for this because you'll see it a lot!

Usually, an error is mistakenly cleared in this way when someone is handling an error and from within the error handler he or she calls some other routine that causes Visual Basic to execute an Err.Clear. All sorts of things can make Visual Basic execute an Err.Clear. The result in this case is that the error is lost! These kinds of mistakes are really hard to find. They're also really easy to put in—lines of code that cause this to happen, that is!

The Help file under Err Object used to include this Caution about losing the error context.

If you set up an error handler using On Error GoTo and that handler calls another procedure, the properties of the Err object may be reset to zero and zero-length strings. To retain values for later use, assign the values of Err properties to variables before calling another procedure, or before executing Resume, On Error, Exit Sub, Exit Function, or Exit Property statements.

Of course, if you do reset Err.Number (perhaps by using On Error GoTo in the called routine), when you return to the calling routine the error will be lost. The answer, of course, is to preserve, or push, the error context onto some kind of error stack. We do this with Err.Push. It's the first line of code in the error handler—always. (By the way, Visual Basic won't do an Err.Clear on the call to Err.Push but only on its return—guaranteed.) Here's an example of how this push and pop method of error handling looks in practice:

Private Sub Command1_Click()

    On Error GoTo error_handler:

    VBA.Err.Raise 42

    Exit Sub

error_handler:

    Err.Push
    Call SomeFunc
    Err.Pop
    MsgBox Err.Description
    Resume Next

End Sub

Here we're raising an error (42, as it happens) and handling it in our error handler just below. The message box reports the error correctly as being an Application Defined Error. If we were to comment out the Err.Push and Err.Pop routines and rerun the code, the error information would be lost and the message box would be empty (as Err.Number and Err.Description have been reset to some suitable "nothing"), assuming the call to SomeFunc completes successfully. In other words, when we come to show the message box, there's no outstanding error to report! (The call to Err.Push is the first statement in the error handler. This is easy to check for during a code review.)

Note

If we assume that Visual Basic itself raises exceptions by calling Err.Raise and that Err.Raise simply sets other properties of Err, such as Err.Number, our own Err.Number obviously won't be called to set VBA.Err properties (as it would if we simply had a line of code that read, say, Err.Number = 42). This is a pity because if it did call our Err.Number, we could detect (what with our Err.Number being called first before any other routines) that an error was being raised and automatically look after preserving the error context; that is, we could do an Err.Push automatically without having it appear in each error handler.

All sound good to you? Here's a sample implementation of a new Err object that contains Pop and Push methods:

In a class called ErrObject

Private e() As ErrObjectState

Private Type ErrObjectState

    Description As String
    HelpContext As Long
    HelpFile    As String
    Number      As Long

End Type

Public Property Get Description() As String

    Description = VBA.Err.Description

End Property

Public Property Let Description(ByVal s As String)

    VBA.Err.Description = s

End Property

Public Property Get HelpContext() As Long

    HelpContext = VBA.Err.HelpContext

End Property

Public Property Let HelpContext(ByVal l As Long)

    VBA.Err.HelpContext = l

End Property

Public Property Get HelpFile() As String

    HelpFile = VBA.Err.HelpFile

End Property

Public Property Let HelpFile(ByVal s As String)

    VBA.Err.HelpFile = s

End Property

Public Property Get Number() As Long

    Number = VBA.Err.Number

End Property

Public Property Let Number(ByVal l As Long)

    VBA.Err.Number = l

End Property

Public Property Get Source() As String

    Source = VBA.Err.Source

End Property

Public Property Let Source(ByVal s As String)

    VBA.Err.Source = s

End Property

Public Sub Clear()

    VBA.Err.Clear

    Description = VBA.Err.Description
    HelpContext = VBA.Err.HelpContext
    HelpFile = VBA.Err.HelpFile
    Number = VBA.Err.Number

End Sub

Public Sub Push()

    ReDim Preserve e(UBound(e) + 1) As ErrObjectState

    With e(UBound(e))

        .Description = Description
        .HelpContext = HelpContext
        .HelpFile = HelpFile
        .Number = Number

    End With

End Sub

Public Sub Pop()

    With e(UBound(e))

        Description = .Description
        HelpContext = .HelpContext
        HelpFile = .HelpFile
        Number = .Number

    End With

    If UBound(e) Then
        ReDim e(UBound(e) - 1) As ErrObjectState
    Else
        VBA.Err.Raise Number:=28 ' Out of stack space - underflow
    End If

End Sub

Private Sub Class_Initialize()

    ReDim e(0) As ErrObjectState

End Sub

Private Sub Class_Terminate()

    Erase e()

End Sub

In Sub Main

Set Err = New ErrObject

In Global Module

Public Err As ErrObject

As you can see, our new Err object maintains a stack of a user-defined type (UDT) called ErrObjectState. An instance of this type basically holds information from the last error. In Sub Main we create our only ErrObject—note that it's called Err. This means that calls to methods like Err.Number will be directed to our object. In other words, Err refers to our instance of ErrObject and not the global instance VBA.Err. This means, of course, that we have to provide stubs for all the methods that are normally part of the global Err object: Number, Description, Source, and so on.

Note that we've left LastDLLError off the list. This is because when we pop the stack we'd need to write a value back into VBA.Err.LastDLLError and, unfortunately, this is a read-only property!

Another object we replace is the Debug object. We do this because we sometimes want to see what debug messages might be emitting from a built executable.

As you know, "normal" Debug.Print calls are thrown away by Visual Basic when your application is running as an executable; "special" Debug.Print calls, however, can be captured even when the application is running as an executable. Replacing this object is a little trickier than replacing the Err object because the Debug object name cannot be overloaded; that is,you have to call your new object something like Debugger. This new object can be designed to write to Visual Basic's Immediate window so that it becomes a complete replacement for the Debug object. Chapter 6 shows how you can write your own Assert method so that you can also replace the Debug object's Assert method.


Tip 12: Check DLL version errors.

Debugging and defensive programming techniques can be used even in postimplementation. We always protect our applications against bad dynamic links (with DLLs and with ActiveX components such as OCXs) by using another internal tool. For a great example of why you should do this, see Chapter 8, Steve Overall's chapter about the Year 2000.

One of the really great things about Windows is that the dynamic linking mechanism, which links one module into another at run time, is not defined as part of some vendor's object file format but is instead part of the operating system itself. This means, for example, that it's really easy to do mixed language programming (whereas with static linking it's really hard because you're at the mercy of some vendor's linker—you just have to hope that it will understand). Unfortunately, this same mechanism can also get you into trouble because it's not until run time that you can resolve a link. Who knows what you'll end up linking to in the end—perhaps to an old and buggy version of some OCX. Oops!

By the way, don't use a GUID (explained below) to determine the version of any component. The GUID will almost always stay the same unless the object's interface has changed; it doesn't change for a bug fix or an upgrade. On the other hand, the object's version number should change whenever the binary image changes; that is, it should change whenever you build something (such as a bug fix or an interface change) that differs in any way from some previous version. The version number, not the GUID, tells you whether you're using the latest incarnation of an object or application.

Because it affects your application externally, this kind of versioning problem can be extremely hard to diagnose from afar (or for that matter, from anear).

So how do you check DLL version numbers? There are two ways of doing this since the underlying VERSIONINFO resource (which contains the version information) usually contains the information twice. A VERSIONINFO resource contains the version number both as a string and as a binary value. The latter is the most accurate, although the former is the one shown to you by applications like Microsoft Windows Explorer.

Here's an example. On my machine, OLEAUT32.DLL has a version number reported by Windows Explorer of 2.30.4261, whereas Microsoft System Information (MSINFO32.EXE version 2.51) shows the same file having a version number of 2.30.4261.1. Is Windows Explorer missing a .1 for some reason? Let's experiment further to find out. Here's another one—OPENGL32.DLL. (You can follow along if you have the file on your hard disk.) Windows Explorer reports this DLL as being version 4.00, yet Microsoft System Information says it's version 4.0.1379.1. Which is right? They both are, sort of. One version number is the string (4.00); the other is the binary (4.0.1379.1). As I mentioned earlier, the binary version number is more accurate, which is why it's used by the Windows' versioning API, Microsoft System Information, and of course all good installation program generators like InstallShield.

Globally Unique Identifiers (GUIDs)

A GUID (Globally Unique Identifier) is a 128-bit integer that can be used by COM (Component Object Model) to identify ActiveX components. Each GUID is guaranteed to be unique in the world. GUIDs are actually UUIDs (Universally Unique Identifiers) as defined by the Open Software Foundation's Distributed Computing Environment. GUIDs are used in Visual Basic mainly to identify the components you use in your projects (referenced under the References and Components items of the Project menu) and to help ensure that COM components do not accidentally connect to the "wrong" component, interface, or method even in networks with millions of component objects. The GUID is the actual name of a component, not the string you and I use to name it, or its filename. For example, a component we've probably all used before is F9043C88-F6F2-101A-A3C9-08002B2F49FB. You and I most likely refer to this component as the "Microsoft Common Dialog Control," or more simply, COMDLG32.OCX. (I have two of these on my machine, both with the same GUID. Their versions are different, however. One is 5.00.3112, and the other is 6.00.8169. Which do you link with?)

To determine a component's GUID, look in your project's VBP file. You'll see something like this if you use the Common Dialog control:

Object={F9043C88-F6F2-101A-A3C9-08002B2F49FB}#1.2#0;
COMDLG32.OCX

Visual Basic creates GUIDs for you automatically (for every ActiveX control you build). If you want to create them externally to Visual Basic, you can use either GUIDGEN.EXE or UUIDGEN.EXE, Microsoft utilities that come with the Visual C++ compiler, the ActiveX SDK, and on the Visual Basic 6 CD. You'll also find a Visual Basic program to generate GUIDs (in Chapter 7, my chapter on type libraries).

To see some sample code that determines a file's real version number, refer to the VB98\WIZARDS\PDWIZARD\SETUP1 source code. Everything you need is in there and ready for you to borrow!

By the way, when you compile your project, Visual Basic sets the string and binary version numbers to the number you enter on the Make tab of the Project Properties dialog box. For example, if you set your version number to, say, 1.2.3 in the Project Properties dialog box and build the EXE (or whatever) and then examine its version number using Windows Explorer and Microsoft System Information, you'll find that Windows Explorer reports the version number as 1.02.0003 while Microsoft System Information reports it as 1.2.0.3.

Backward Compatibility

Once you've got your version-checking code in place should you assume backward compatibility?

I'd say you should normally assume that 1.2.3 is simply a "better" 1.2.2 and so forth, although again I urge you to see Chapter 8 to find out what Steve has to say about OLEAUT32.DLL and to see a DLL that changed its functionality, not just its version number.

Normally a version number increment shows that the disk image has been altered, maybe with a bug fix. Bottom line: whenever the binary image changes, the version number should change also. An interface GUID change means that some interface has changed. Bottom line: whenever the interface changes, the GUID (and of course the version number) must change. If you don't have an interface, you're probably working with a DLL; in addition to being a binary file, this DLL has both entry points and behavior. Bottom line: if the behavior of a DLL changes or an entry point in it is modified, you must also change the filename.

To make the version numbers of your software even more accessible to your consumers, you might want to build an interface into your components, maybe called VERSIONINFO, that returns the version number of the EXE or DLL. All it would take is one Property Get:

Public Property Get VersionNumber() As String
    VersionNumber = App.Major & "." & App.Minor & "." & App.Revision
End Property


Tip 13: Use Microsoft System Information (MSINFO32.EXE) when you can.

When you're trying to help a user with some problem (especially if you're in support), you often need to know a lot of technical stuff about the user's machine, such as what is loaded into memory or how the operating system is configured. Getting this information out of the user, even figuring out where to find it all in the first place, can be time-consuming and difficult. (Asking the user to continually hit Ctrl+Alt+Delete in an attempt to bring up the Task List and "see" what's running can be a dangerous practice: User: "Oh, my machine's rebooting." Support: "What did you do?" User: "What you told me to do—hit Ctrl+Alt+Delete again!") Microsoft thought so too, so they provided their users with an application to gather this information automatically: Microsoft System Information (MSINFO32.EXE). The good news is that you can use this application to help your customers.

Microsoft System Information comes with applications such as Microsoft Word and Microsoft Excel. If you have one of those applications installed, you're almost certain to have Microsoft System Information installed too. It also ships with Visual Basic 6. If you haven't seen this applet before, choose About Microsoft Visual Basic from the Help menu and click the System Info button. You'll see a window similar to Figure 1-5.

Click to view at full size.

Figure 1-5 Running MSINFO32.EXE opens the Microsoft System Information application

The bottom line is that if your user is a Microsoft Office user or has an application such as Microsoft Excel installed, Microsoft System Information will be available. All you need to do then to provide the same information on the user's system is to run the same application!

To determine whether you've got this application to work with, look in the following location in the Registry:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Shared Tools\MSInfo\Path

In the following example, we use the registration API in ADVAPI32.DLL to retrieve the value of the Registry key. We can then check to see whether the application really exists. If it does, Shell it!

Declaration Section

Option Explicit

Private Const REG_SZ                    As Long = 1
Private Const ERROR_SUCCESS             As Long = 0
Private Const HKEY_LOCAL_MACHINE        As Long = &H80000002
Private Const STANDARD_RIGHTS_ALL       As Long = &H1F0000
Private Const KEY_QUERY_VALUE           As Long = &H1
Private Const KEY_ENUMERATE_SUB_KEYS    As Long = &H8
Private Const KEY_NOTIFY                As Long = &H10
Private Const SYNCHRONIZE               As Long = &H100000
Private Const READ_CONTROL              As Long = &H20000
Private Const STANDARD_RIGHTS_READ      As Long = (READ_CONTROL)
Private Const KEY_READ                  As Long = _
                                           ((STANDARD_RIGHTS_READ _
                                           Or KEY_QUERY_VALUE _
                                           Or KEY_ENUMERATE_SUB_KEYS _
                                           Or KEY_NOTIFY) _
                                           And (Not SYNCHRONIZE))

Private Declare Function WinRegOpenKeyEx Lib "advapi32.dll" _
Alias "RegOpenKeyExA" (ByVal hKey As Long, _
                       ByVal lpSubKey As String, _
                       ByVal ulOptions As Long, _
                       ByVal samDesired As Long, _
                       phkResult As Long) As Long

Private Declare Function WinRegQueryValueEx Lib _
"advapi32.dll" Alias "RegQueryValueExA" _
                     (ByVal hKey As Long, _
                      ByVal lpValueName As String, _
                      ByVal lpReserved As Long, _
                      lpType As Long, lpData As Any, _
                      lpcbData As Long) As Long

Private Declare Function WinRegCloseKey Lib "advapi32" _
Alias "RegCloseKey" (ByVal hKey As Long) As Long

Form Load Event

Private Sub Form_Load()

    Dim hKey   As Long
    Dim lType  As Long
    Dim Buffer As String

    ' Need some space to write string into - DLL routine
    ' expects us to allocate this space before the call.
    Buffer = Space(255)

    ' Always expect failure!
    cmdSystemInfo.Visible = False

    ' This will work if Microsoft System Information is installed.
    If WinRegOpenKeyEx( _
                 HKEY_LOCAL_MACHINE _
                , "SOFTWARE\Microsoft\Shared Tools\MSInfo" _
                , 0 _
                , KEY_READ _
                , hKey _
                ) = ERROR_SUCCESS Then

        ' Read the Path value - happens to include the filename
        ' too, e.g.,
        ' "C:\Program Files\Common Files\Microsoft Shared\
        ' MSinfo\msinfo32.exe".
        If WinRegQueryValueEx( _
                              hKey _
                             , "Path" _
                             , 0 _
                             , lType _
                             , ByVal Buffer _
                             , Len(Buffer) _
                             ) = ERROR_SUCCESS Then
            ' Make sure we read a string back. If we did...
            If lType = REG_SZ Then
                ' Make sure the Registry and reality are in
                ' alignment!
                ' Note: Using FileAttr() means you're
                ' suffering from paranoia<g>.
                If Dir$(Buffer) <> "" Then
                    ' Put the path into the button's Tag
                    ' property and make the button visible.
                    cmdSystemInfo.Tag = Buffer
                    cmdSystemInfo.Visible = True
                End If
            End If

        End If

        ' We open - we close.
        Call WinRegCloseKey(hKey)

    End If

End Sub

Button Click Event

Private Sub cmdSystemInfo_Click()

    ' If we got clicked, we must be visible and therefore
    ' must have our Tag property set to the name of the
    ' Microsoft System Information application - Shell it!
    Call Shell(cmdSystemInfo.Tag, vbNormalFocus)

End Sub

In the code above, as the form loads (maybe this is an About box?) it detects whether or not Microsoft System Information exists. If it does, the form makes a command button visible and sets its Tag property to point to the program. When the form becomes visible, the button either will or won't be visible. If it is visible, you have Microsoft System Information on your machine. When you click the button, it simply calls Shell with the value in its Tag property. For more information on the APIs used in this example, see the appropriate Win32 documentation.

One of the neat little extras that came first with Visual Basic 5 was the little wizard dialog "thang" that allowed you to add standard dialog boxes to your application. One of these standard dialog boxes is an About dialog box. You'll notice that the About dialog box comes complete with a System Info button. The dialog box displays the Microsoft System Information utility using code similar to that shown above. (I think ours is cooler so I've left it here in the second edition.) This raises an interesting question, however. Is Microsoft implicitly giving you and me permission to ship MSINFO32.EXE (and anything that it needs) with an EXE? I'm afraid I don't know the answer to this one—sorry!


Tip 14: Treat error handling like transaction processing.

When you hit an error, always attempt to bring the application back to a known and stable condition; that is, roll back from the error. To do this, you'll need to handle errors locally (to roll back within the scope of the erroring procedure) and more globally by propagating the error back up through each entry in the call chain.

Here's how you proceed. When your most local (immediate) error trap gets hit, make sure you clean up as required locally first. For example, make sure you close any files that you opened in this routine. Once that's done, and if this routine is not an event handler, reraise the error (in reality, you might raise some other error here) and repeat this process for each previous stack frame (a stack frame refers to an entry in the call chain); that is, continue this process for each preceding call until you get back up to an event handler. If you've cleaned up locally all the way through the call chain and if you had an error handler for each stack frame (so that you didn't jump over some routines), you should now have effectively rolled back from the error. It will seem as though the error never really happened. Note that by not reporting errors from anywhere other than an event handler, you will not have shown your user a stream of message boxes.

Localized error handling might need error handling itself. Look at the following code fragment:

On Error GoTo Error_Handler:

    Dim nFile As Integer

    nFile = FreeFile
    Open "c:\time.txt" For Output Access Write As nFile
    Print #nFile, Time$
    Close nFile

    Exit Sub

Error_Handler:

    ' Roll back!
    Close nFile

    Exit Sub

Imagine you have opened a file and are attempting to roll back in your error handler. How do you know whether or not you opened the file? In other words, did the error occur before or after the line of code that opens the file? If you attempt to close the file and it's not open, you'll cause an error—but if it's open, you don't want to leave it open as you're trying to roll back! I guess you could use Erl to determine where your code erred, but this implies that you're editing line numbered source code—yuck. (You'll recall from Tip 2 that we added line numbers only to the code for the final EXE, not to the code we're still editing.) Probably the best way to determine what did or did not get done is to limit the possibilities; that is, keep your routines small (so that you have only a small problem domain). Of course, that's not going to help us here. What we need to do is apply a little investigation!

Given this type of problem, you're probably going to have to test the file handle to see whether it points to an open file. In the code above, we would probably use FileAttr(nFile, 1) to determine whether or not the file nFile is open for writing. If the file is not open, FileAttr raises an exception (of course). And obviously, you can't handle this locally because you can't set an error trap from within an error trap unless your error handling is in another routine! (Refer to Tip 5 for details.)


Tip 15: Don't test your own software or write your own test plans.

Do you have dedicated testers where you work? Possibly not—not many companies do. Many companies say they "can't afford such a luxury." Well, in my opinion, they're a luxury that's really worth it (as many of the leading software development companies in the world already know).

Independent testers should (and often do) exhibit the following characteristics:

  • Are impartial

  • Are less informed about the usage and the type of input your code expects

  • Are usually more knowledgeable about the usage and the type of input your code doesn't expect

  • Are more likely than you to spend time trying to break code

  • Are typically more leery of your interfaces and more critical of your coupling

  • Are into doing you damage and breaking your code

  • Are more informed than you about system limits

  • Unlike you, actually want to find bugs in your software.

From time to time, Microsoft talks about its ratio of developers to testers: around 1:1. You do the math; for every programmer there's a tester. In fact, rumor has it that some developers occasionally get shifted to being testers. This could happen if a developer consistently develops very buggy software. Nothing like a shift to testing to improve one's knowledge and appreciation of what good solid code involves.


Tip 16: Stress test your applications.

Years ago, the Windows SDK (Software Development Kit) shipped with an applet named SHAKER.EXE. This applet simply ran around allocating and releasing memory blocks. When and what it actually allocated or released was random!

What was it for, then? Well, before the days of protect mode and virtual memory addressing, you could access any arbitrary memory location through a simple pointer (using C as a programming language, of course). Often, and erroneously, these pointers would be stored in nonrefreshed static variables as an application yielded control to the operating system. This access—or similar access—would cause the problems for which SHAKER.EXE was used to try to uncover.

In between handling one event and a subsequent one, Windows could move (as it can now) both your code and data around. If you'd used a static pointer to, say, point to some data, you'd quickly discover that it was no longer pointing to what you intended. (Modern virtual addressing methods make this problem go away.) So what was the point of SHAKER.EXE? It turned out that, back then, even though your application was being naughty and had stored a static pointer, you didn't know it most of the time; the Windows memory manager hadn't moved your data around between your handling of two events. The bottom line was that you didn't really know you had a problem until memory moved, and on your machine, that rarely, if ever, happened. Customers, however, did see the problem because they were running both your application and others and had loaded their systems to a point that the memory manager was starting to move memory blocks around to accommodate everyone. The whole thing was like attempting to hold a party in a small closet. Initially, everyone had plenty of room. As more people arrived and the closet filled up, however, some of the guests were bound to get their feet stepped on sooner or later. SHAKER.EXE shook the operating system on the developer's machine until something fell off!

OK, so why the history lesson? Basically, the lesson is a good one and one we can still use. In fact, an associated application, named STRESS.EXE, still ships in Visual C++. (See Figure 1-6.)

Figure 1-6 Stress me (STRESS.EXE)

Like SHAKER.EXE, STRESS.EXE is used to make the operating system appear more loaded or busy than it actually is. For example, by using STRESS.EXE you can allocate all of your machine's free memory, making it look really loaded—or, reading from Tip 6 on, you can find out what happens when you run out of file handles.

Tools such as STRESS.EXE can present your code with a more realistic, perhaps even hostile, environment in which to work. Such conditions can cause many hidden problems to rise to the surface—problems you can fix at that point instead of later in response to a client's frustrated phone call. I'd certainly recommend using them.


Tip 17: Use automated testing tools.

See Chapter 9, ""Well, at Least It Compiled OK!"" for coverage of this broad and very important subject.


Tip 18: Consider error values.

Let's suppose you still want to return an indication of success from a function (instead of using exceptions). What values would you use to indicate whether or not something worked?

Normally, 0 (or False) is returned for failure, and -1 (True) for success. What are the alternatives? Some programmers like to return 0 for success and some other value for failure—the reason for failure is encoded in the value being returned. Other programmers prefer to return a negative value for failure that again encodes the reason.

By using the first alternative, we can quickly come up with some pretty weird-looking code:

If CreateThing() <> True Then ' It worked!

or

If Not CreateThing() Then ' It worked!

or

If CreateThing() = False Then ' It worked!

or

If CreateThing() = SUCCESS Then ' It worked!

SUCCESS, of course, is defined as 0. To capture failure, you can't just do the same, though:

If Not CreateThing() Then ' It worked!
Else
    ' Failed!
    ' What do we do?
End If

Here the reason for failure is lost. We need to hold it in some variable:

nResult = CreateThing()

If nResult <> SUCCESS Then
    ' Failed!
    ' What do we do?
End If

All very messy, especially where the language lacks the ability to do an assignment in a conditional expression (as is the case in Visual Basic and is not the case in C).

Consider someone writing the test using implicit expression evaluation:

If CreateThing() Then

If CreateThing works, it returns 0, which causes the conditional not to execute any code in the body of the compound statement. Yikes! Imagine what code fails to execute all because someone forgot to test against SUCCESS.

Because any nonzero value is evaluated as True (in an If), using a value other than 0 (say, a negative value) to indicate failure can be equally dangerous. Given that in any conditional expression you don't have to test against an explicit value and that nonzero means execute the compound statement, the language conspires against you here not to use 0 as a code indicating success.

I'd advise sticking to True meaning success and False meaning failure. In the case of failure, I'd implement a mechanism such as the one used in C (errno) or perhaps Win32's GetLastError. The latter returns the value of the last error (easily implemented in a project—you could even add a history feature or automatic logging of errors).


Tip 19: Tighten up Visual Basic's type checking.

Visual Basic itself doesn't always help you detect errors or error conditions. For example, consider the following code fragment:

Private Sub Fu(ByVal d As Date)
    .
    .
    .

End Sub

Call Fu("01 01 98")

Is this code legal? If you ask around, quite often you'll find that developers say no, but it is perfectly legal. No type mismatch occurs (something that worries those who suspect this is illegal).

The reason the code is legal lies in Visual Basic itself. Visual Basic knows that the Fu procedure requires a Date type argument, so it automatically tries to convert the string constant "01 01 98" into a Date value to satisfy the call. If it can convert the string constant, it will. In other words, it does this kind of thing:

' The call .
                   .
                   .
'
' Call Fu("01 01 98")
'
' Equates to …
'
Const d As String = "01 01 98"

If IsDate(d) Then

    Dim Local_d As Date

    Local_d = CDate(d)

    Call Fu(Local_d)

Else

    Err.Raise Number:=13

End If

Now you see that Visual Basic can make the call by performing the cast (type coercion) for you. Note that you can even pass the argument by reference simply by qualifying the argument with the ByRef keyword, as in Call Fu(ByRef "01 01 98"). All you're passing by reference, in fact, is an anonymous variable that Visual Basic creates solely for this procedure call. By the way, all ByVal arguments in Visual Basic are passed by reference in this same fashion. That is, when it encounters a ByVal argument, Visual Basic creates an anonymous variable, copies the argument into the variable, and then passes a reference to the variable to the procedure. Interestingly, a variable passed by reference must be of the correct type before the call can succeed. (This makes perfect sense given that Visual Basic can trust itself to create those anonymous variables with the correct type; it can't trust user-written code to do the right thing, so Visual Basic has to enforce by-reference type checking strictly.)

So what's wrong with this automatic type coercion anyway? I hope you can see that the problem in the case above is that the cast is not helpful. We're passing an ambiguous date expression but receiving an actual, unambiguous date. This is because all date variables are merely offsets from December 30, 1899, and therefore unambiguous (for example, 1.5 is noon on December 31, 1899). There's no way "inside" of Fu to detect this fact and to refuse to work on the data passed. (Maybe that's how it should be? Maybe we should rely on our consumers to pass us the correct data type? No, I don't think so.)

One way to fix this [part of the] problem is to use Variants, which are some of the few things I normally encourage people to use. Have a look at this:

Call Fu("01 01 98")

Private Sub Fu(ByVal v As Variant)

    Dim d As Date

    If vbString = VarType(v) Then

        If True = IsDate(CStr(v)) Then

            If 0 = InStr(1, CStr(v), CStr(Year(CDate(v))), 1) Then
                Err.Raise Number:=13
            Else
                d = CDate(v)
            End If

        End If

    End If

    ' Use d here…

End Sub

The good thing about a Variant (and the bad?) is that it can hold any kind of data type. You can even ask the Variant what it's referencing by using VarType, which is very useful. Because we type the formal argument as Variant we'll receive in it a type equal to the type of the expression we passed. In the code above, VarType(v) will return vbString, not vbDate.

Knowing this, we can check the argument types using VarType. In the code above, we're checking to see if we're being passed a string expression. If the answer is yes, we're then checking to see that the string represents a valid date (even an ambiguous one). If again the answer is yes, we convert the input string into a date and then use InStr to see if the year in the converted date appears in the original input string. If it doesn't, we must have been passed an ambiguous date.

Here's that last paragraph rephrased and broken down a bit. Remember that a Date always holds an exact year because it actually holds an offset from December 30, 1899. Therefore, Year(a_Date_variable) will always give us back a full four-digit year (assuming that a_Date_variable represents a date after the year 999). On the other hand, the string that "seeds" the Date variable can hold only an offset—98 in the example. Obviously then, if we convert 98 to a Date (see Chapter 8 for more on this topic), we'll get something like 1998 or 2098 in the resulting Date variable. When converted to a string, those years are either "1998" or "2098"—neither of which appears in "01 01 98." We can say with some conviction, therefore, that the input string contains an ambiguous date expression, or even that its data type ("ambiguous date") is in error and will throw a type mismatch error.

All this date validation can be put inside a Validate routine, of course:

Private Sub Fu(ByVal v As Variant)

    Dim d As Date

    Call Validate(v, d)

    ' Use d here _ we don't get here if there's a problem with 'v'...

End Sub

In this Validate routine d is set to cast(v) if v is not ambiguous. If it is ambiguous, an exception is thrown. An exciting addition to this rule is that the same technique can also be applied to Visual Basic's built-in routines via Interface Subclassing.

How often have you wanted an Option NoImplicitTypes? I have, constantly. Here's how you can almost get to this situation:

Private Sub SomeSub()

    MsgBox DateAdd("yyyy", 100, "01 01 98")

End Sub

Public Function DateAdd( _
                         ByVal Interval As String _
                       , ByVal Number As Integer _
                       , ByVal v As Variant _
                      )

    Call Vali_Date(v)

    DateAdd = VBA.DateTime.DateAdd(Interval, Number, CDate(v))

End Function

Private Sub Vali_Date(ByVal v As Variant)

    ' If 'v' is a string containing a valid date expression ...
    If vbString = VarType(v) And IsDate(CStr(v)) Then

        ' If we've got a four digit year then we're OK,
        ' else we throw an err.
        If 0 = InStr(1, CStr(v), _
                     Format$(Year(CDate(v)), "0000"), 1) Then
            Err.Raise Number:=13
        End If

    End If

End Sub

In this code, the line MsgBox DateAdd(...) in SomeSub will result in a runtime exception being thrown because the date expression being passed is ambiguous ("01 01 98"). If the string were made "Y2K Safe"—that is, 01 01 1998—the call will complete correctly. We have altered the implementation of DateAdd; you could almost say we inherited it and beefed up its type checking.

Obviously this same technique can be applied liberally so that all the VBA type checking (and your own type checking) is tightened up across procedure calls like this. The really nice thing about doing this with Visual Basic's routines is that instead of finding, say, each call to DateAdd to check that its last argument is type safe, you can build the test into the replacement DateAdd procedure. One single replacement tests all calls. In fact, you can do this using a kind of Option NoImplicitTypes.

Use this somewhere, perhaps in your main module:

#Const NoImplicitTypes = True

Then wrap your validation routines appropriately:

Private Sub Vali_Date(ByVal v As Variant)

    #If NoImplicitTypes = True Then

    ' If 'v' is…
    If …
    End If

    #End If

End Sub

You now almost have an Option NoImplicitTypes. I say almost because we can't get rid of all implicit type conversions very easily (that's why I used "[part of the]" earlier). Take the following code, for example:

Dim d As Date

d = txtEnteredDate.Text

Your validation routines won't prevent d from being assigned an ambiguous date when txtEnteredDate.Text is "01 01 98", but at least you're closer to Option NoImplicitTypes than you would be without the routines.

Actually, at TMS we use a DateBox control, and even that control cannot stop this sort of use. (See Chapter 8 for more discussion about this, and see the companion CD for a demonstration.) A DateBox returns a Date type, not a Text type, and it's meant to be used like this:

Dim d As Date

d = dteEnteredDate.Date

Of course, it can still be used like this:

Dim s As String

s = dteEnteredDate.Date

Hmm, a date in a string! But at least s will contain a non-Y2K-Challenged date.

Might Microsoft add such an Option NoImplicitTypes in the future? Send them e-mail asking for it if you think it's worthwhile (mswish@microsoft.com).

A Not-Too-Small Aside into Smart Types, or "Smarties"

Another way to protect yourself against this kind of coercion is to use a smart type (we call these things Smarties, which is the name of a candy-coated confection) as an lvalue (the thing on the left-hand side of the assignment operator). A smart type is a type with vitamins added, one that can do something instead of doing nothing. The difference between smart types and "dumb" types is a little like the difference between public properties that are implemented using variables versus public properties implemented using property procedures. Here's some test code that we can feed back into the code above that was compromised:

Dim d As New iDate

d = txtEnteredDate.Text

Note that we're using a slightly modified version of the code here, in which d is defined as an instance (New) of iDate instead of just Date. (Of course, iDate means Intelligent Date.) Here's the code behind the class iDate:

In a class called iDate

Private d As Date

Public Property Get Value() As Variant

    Value = CVar(d)

End Property

Public Property Let Value(ByVal v As Variant)

    If vbDate = VarType(v) Then
        d = CDate(v)
    Else
        Err.Raise 13
    End If

End Property

OK then, back to the code under the spotlight. First you'll notice that I'm not using d.Value = txtEnteredDate.Text. This is because I've nominated the Value property as the default property. (Highlight Value in the Code window, select Procedure Attributes from the Tools menu, click Advanced >> in the Procedure Attributes dialog box, and then set Procedure ID to (Default).) This is the key to smart types, or at least it's the thing that makes them easier to use. The default property is the one that's used when you don't specify a property name. This means that you can do stuff like Print Left$(s, 1) instead of having to do Print Left$(s.Value, 1). Cool, huh? Here's that test code again:

Dim d As New iDate

d = txtEnteredDate.Text

If you bear in mind this implementation of an iDate, you see that this code raises a Type Mismatch exception because the Value Property Let procedure, to which the expression txtEnteredDate.Text is passed as v, now validates that v contains a real date. To get the code to work we need to do something a little more rigid:

Dim d As New iDate

d = CDate(txtEnteredDate.Text)

Just what the doctor ordered. Or, in the case of a date, does this perhaps make the situation worse? One reason why you might not want to explicitly convert the text to a date is that an ambiguous date expression in txtEnteredDate.Text is now converted in a way that's hidden from the validation code in the d.Value Property Let procedure. Perhaps we could alter the code a little, like this:

Public Property Let Value(ByVal v As Variant)

    If vbString = VarType(v) And IsDate(CStr(v)) Then

        ' If we've got a four digit year then we're OK,
        ' else we throw an err.
        If 0 = InStr(1, CStr(v), _
                     Format$(Year(CDate(v)), "0000"), 1) Then
            Err.Raise Number:=13
        End If

    End If

    d = CDate(v)

End Property

Here I've basically borrowed the code I showed earlier in this chapter which checks whether a date string is ambiguous. Now the following code works only if txtEnteredDate.Text contains a date like "01 01 1900":

Dim d As New iDate

d = txtEnteredDate.Text

Another cool thing about Smarties is that you can use them within an existing project fairly easily, in these different ways:

  1. Add the class file(s) that implement your smart types.

  2. Use search and replace to turn dumb types into Smarties.

  3. Run your code and thoroughly exercise (exorcise) it to find your coercion woes.

  4. Use search and replace again to swap back to dumb types (if you want).

Actually, I'll come clean here—it's not always this easy to use Smarties. Let's look at some pitfalls. Consider what happens when we search for As String and replace with As New iString. For one thing we'll end up with a few procedure calls like SomeSub(s As New iString), which obviously is illegal. We'll also get some other not-so-obvious—dare I say subtle?—problems.

Say you've got SomeSub(ByVal s As iString); you might get another problem here because now you're passing an object reference by value. ByVal protects the variable that you're passing so that it cannot be altered in a called procedure (a copy is passed and possibly altered in its place). The theory is that if I have s = Time$ in the called procedure, the original s (or whatever it was called in the calling procedure) still retains its old value. And it does; however, remember that the value we're protecting is the value of the variable. In our case that's the object reference, not the object itself. In C-speak, we can't change the object pointer, but because we have a copy of the pointer, we can access and change any of the object's properties. Here's an example that I hope shows this very subtle problem.

These two work the same:

  Private Sub cmdTest_Click()

      Dim s As New iString

      s = Time$

      Call SomeSub(s)

      MsgBox s

  End Sub

  Sub SomeSub(ByRef s As iString)

      s = s & " " & Date$

      MsgBox s

  End Sub

Private Sub cmdTest_Click()

    Dim s As String

    s = Time$

    Call SomeSub(s)

    MsgBox s

End Sub

Sub SomeSub(ByRef s As String)

    s = s & " " & Date$

    MsgBox s

End Sub

The assignment to s in both versions of SomeSub affects each instance of s declared in cmdTest_Click.

These two don't work the same:

 Private Sub cmdTest_Click()

      Dim s As New iString

      s = Time$

      Call SomeSub(s)

      MsgBox s

  End Sub

  Sub SomeSub(ByVal s As iString)

      s = s & " " & Date$

      MsgBox s

  End Sub

Private Sub cmdTest_Click()

    Dim s As String

    s = Time$

    Call SomeSub(s)

    MsgBox s

End Sub

Sub SomeSub(ByVal s As String)

    s = s & " " & Date$

    MsgBox s

End Sub

The assignment to s in the SomeSub on the left still affects the instance of s declared in the cmdTest_Click on the left.

Let me again run through why this is. This happens because we're not passing the string within the object when we pass an iString; we're passing a copy of the object reference. Or, if you like, we're passing a pointer to the string. So it doesn't matter whether we pass the object by reference or by value—the called procedure has complete access to the object's properties.

You also cannot change iString to String in the procedure signature (if you did, you would defeat the purpose of all this, for one thing) and still pass ByRef because you're effectively trying to pass off an iString as a String, and you'll get a type mismatch.

Another area where you'll have problems is in casting (coercion). Consider this:

Private Function SomeFunc(s As iString) As iString

    SomeFunc = s

End Function

Look OK to you? But it doesn't work! It can't work because = s, remember, means = s.Value—a String—and that's not an iString as implied by the assignment to SomeFunc. There's no way Visual Basic can coerce a String into an iString reference. (Maybe this is good because it's pretty strongly emphasized.) Could we coerce a String into an iString reference if we wrote a conversion operator (CiStr, for example)? Yes, but that would be overkill because we've already got a real iString in the preceding code. What we need to do is change the code to Set SomeFunc = s. Set is the way you assign an object pointer to an object variable. Anyway, it's simply a semantics change and so should be rejected out of hand. What we need is some way to describe to the language how to construct an iString from a String and then assign this new iString—not using Set—to the function name. (This is all getting us too close to C++, so I'll leave this well alone, although you might want to consider where you'd like Visual Basic to head as a language).

Anyway, you can see that this is getting messy, right? The bottom line is that you can do a good job of replacing dumb types with Smarties, but it's usually something that's best done right from the start of a project. For now, let's look at something that's easier to do on existing projects: another slant on type enforcement.

Type Checking Smarties How do you determine whether you're dealing with an As Object object or with a Smartie? Easy—use VarType. Consider this code; does it beep?

Dim o As New iInteger

If vbObjectiInteger = VarType(o) Then Beep

Normally all object types return the same VarType value (vbObject or 9), so how does VarType know about Smarties (assuming that vbObjectiInteger hasn't also been defined as 9)? Simple; see Tip 4. We subclass VarType and then add the necessary intelligence we need for it to be able to differentiate between ordinary objects and Smarties. For example, VarType might be defined like this

Public Function VarType(ByVal exp As Variant) _
    As Integer ' vbVarType++

    Select Case VBA.VarType(exp)

        Case vbObject:

            Select Case TypeName(exp)

                Case "iInteger"
                    VarType = vbObjectiInteger

                Case "iSingle"
                    VarType = vbObjectiSingle

                Case Else
                    VarType = VBA.VarType(exp)

            End Select

        Case Else

            VarType = VBA.VarType(exp)

    End Select

End Function

The constants vbObjectiInteger, vbObjectiSingle, etc. are defined publicly and initialized on program start-up like this:

Public Sub main()

    vbObjectiInteger = WinGlobalAddAtom(CreateGUID)
    vbObjectiSingle  = WinGlobalAddAtom(CreateGUID)
    ' Etc…

    DoStartup

End Sub

WinGlobalAddAtom is an alias for the API GlobalAddAtom. This Windows API creates a unique value (in the range &HC000 through &HFFFF) for every unique string you pass it, and hopefully there will be no future clashes with whatever VarType will return. (So we have a variable constant: variable in that we don't know what GlobalAddAtom will return when we call it for the first time, but constant in that on subsequent calls GlobalAddAtom will return the same value it returned on the first call). It's basically a hash-table "thang." I want a unique value for each Smartie type I use, so I must pass a unique string to GlobalAddAtom. I create one of these by calling the CreateGUID routine documented in my Chapter 7, "Minutiae: Some Stuff About Visual Basic." This routine always returns a unique GUID string (something like C54D0E6D-E8DE-11D1-A614-0060806A9738), although in a pinch you could use the class name. The bottom line is that each Smartie will have a unique value which VarType will recognize and return!

Why not just use any old constant value? Basically I want to try to be less predictable (clashable with) and more unique, although one downside is this: because I cannot initialize a constant in this way, those vbObjectiInteger and others are variables and could be reassigned some erroneous values later in our code. Actually, that's a lie because they cannot be reassigned a new value. Why not? Because they're Smarties, too. To be precise, they're another kind of Smartie—Longs that can have one-time initialization only. (See Chapter 7 for the code that implements them.)

You might also want to consider whether to enforce at least strict type checking on procedure call arguments and set up some kind of convention within your coding standards whereby parameters are received as Variants (as outlined earlier), tested, and then coerced into a "correct" local variable of the desired type. Another advantage of this scheme is that it mandates a "fast pass by value" handling of arguments and thus can be used indirectly to reduce coupling. It's fast because it's actually a pass by reference!

In the following code, note that despite passing n to Fu by reference (which is the default passing mechanism, of course) we cannot alter it in Fu (if we're disciplined). This is because we work only in that routine on the local variable, d.

In a form (say):

Private Sub cmdTest_Click()

    Dim n As Integer

    n = 100

    Call Fu(n)

End Sub

Public Sub Fu(vD As Variant)

    Dim d As Single

    d = IntegerToReal(vD)

    ' Use d safely ...

End Sub

In a testing module:

Public Function IntegerToReal(ByVal vI As Variant) As Double

    #If True = NoImplicitTypes Then

        Select Case VarType(vI)

            Case vbInteger, vbLong:
                IntegerToReal = CDbl(vI)

            Case Else
                Err.Raise VBErrTypeMismatch

        End Select

    #Else

        IntegerToReal = CDbl(vI)

    #End If

End Function

Here we're implying that our coding standards mandate some type checking. We're allowing integers (both Long and Short) to be implicitly coerced into either Singles or Doubles. Therefore, if we call Fu as Call Fu(100), we're OK. But if we call it as, say, Call Fu("100"), this will fail (if NoImplicitTypes is set to -1 in code using #Const, or in the IDE using the Project Properties dialog box). Note that d in Fu is defined as a Single but that IntegerToReal is returning a Double. This is always OK because an integer will always fit in to a Single; that is, we won't overflow here at all. To speed up the code, perhaps during the final build, you can simply define NoImplicitTypes as 0, in which case the routine forgoes type checking.

Of course, depending on your level of concern (or is that paranoia?), you can turn this up as much as you like. For instance, you could refuse to convert, say, a Long integer to a Single/Double. You're limited only to whatever VarType is limited to, meaning that you can detect any type as long as VarType does.


Tip 20: Define constants using a TypeLib or an Enum.

When you create error values try not to use the so-called Magic Numbers. Thirteen is such a number, as in Err.Raise Number:=13. What does 13 mean? Basically it's a pain to resolve, so attempt always to use more meaningful names.

Visual Basic doesn't come with a set of symbolic constants defined for its own errors so I thought I'd put one together for you. Here's a snippet:

Public Enum vbErrorCodes
    VBErrReturnWithoutGoSub                  = 3
    VBErrInvalidProcedureCall                = 5
    VBErrOverflow                            = 6
    VBErrOutOfMemory                         = 7
    VBErrSubscriptOutOfRange                 = 9
    VBErrThisArrayIsFixedOrTemporarilyLocked = 10
    VBErrDivisionByZero                      = 11
    VBErrTypeMismatch                        = 13
    VBErrOutOfStringSpace                    = 14
    VBErrExpressionTooComplex                = 16
    VBErrCantPerformRequestedOperation       = 17
    VBErrUserInterruptOccurred               = 18
    VBErrResumeWithoutError                  = 20
    VBErrOutOfStackSpace                     = 28
    VBErrSubFunctionOrPropertyNotDefined     = 35
    .
    .
    .
End Enum

Once you've added it to your project, this snippet is browsable via Visual Basic's Object Browser. You'll find the code on the companion CD. To see how you might define constants using a type library, see Chapter 7.


Tip 21: Keep error text in a resource file.

Resource files (RES files) are good things in which to keep your error text and messages, and most C developers use them all the time, especially if they're shipping products internationally. That said, Visual Basic itself uses resource files—recognize some of these sample strings taken from Visual Basic's own resources?

STRINGTABLE FIXED IMPURE
BEGIN
    3                       "Return without GoSub"
    5                       "Invalid procedure call or argument"
    6                       "Overflow"
    7                       "Out of memory"
    .
    .
    .

    13029                   "Sa&ve Project Group"
    13030                   "Sav&e Project Group As..."
    13031                   "Ma&ke %s..."
    .
    .
    .
    23284                   "Compile Error in File '|1', Line |2 : |3"
    .
    .
    .

END

In fact, Visual Basic 6 uses a total of 2,934 resource files.

The %s in string 13031 is used to indicate (to a standard C library function) where a substring should be inserted—the binary name (?.EXE, ?.DLL, ?.OCX) in this case. The |1, |2, and |3 in string 23284 shows where replacement strings should be inserted, this time using a different technique. In fact, this latter technique (which you can use even on the %s strings) can be seen operating if you look at ResolveResString in the Visual Basic source code for SETUP1.VBP. It looks more or less like this (this is slightly tidied up):

'-----------------------------------------------------------
' FUNCTION: ResolveResString
' Reads string resource and replaces given macros with given
' values
'
' Example, given a resource number of, say, 14:
'    "Could not read '|1' in drive |2"
' The call
'    ResolveResString(14, "|1", "TXTFILE.TXT", "|2", "A:")
' would return the string
'    "Could not read 'TXTFILE.TXT' in drive A:"
'
' IN: [nResID]        - resource identifier
'     [vReplacements] - pairs of macro/replacement value
'-----------------------------------------------------------
'
Public Function ResolveResString( _
                                  ByVal nResID As Integer _
                                , ParamArray vReplacements() As Variant _
                                ) As String

    Dim nMacro     As Integer
    Dim sResString As String

    sResString = LoadResString(nResID)

    ' For each macro/value pair passed in ...
    For nMacro = LBound(vReplacements) To UBound(vReplacements) Step 2

        Dim sMacro As String
        Dim sValue As String

        sMacro = CStr(vReplacements(nMacro))
        sValue = vbNullString

        If nMacro < UBound(vReplacements) Then
            sValue = vReplacements(nMacro + 1)
        End If

        ' Replace all occurrences of sMacro with sValue.
        Dim nPos As Integer

        Do
            nPos = InStr(sResString, sMacro)

            If 0 <> nPos Then
                sResString = Left$(sResString, nPos - 1) & _
                             sValue & _
                             Mid$(sResString, nPos + Len(sMacro))
            End If

        Loop Until nPos = 0

    Next nMacro

    ResolveResString = sResString

End Function

To see all this code work, compile the strings and add them to your project. (Save the strings as an RC file, and then run the resource compiler on the RC file like so: C:\rc -r ?.rc. Add the resulting RES file to your application by selecting Add File from the Project menu.) Then add this code to Form1's Load event:

MsgBox ResolveResString( _
                         23284 _
                       , "|1" _
                       , "Fubar.bas" _
                       , "|2" _
                       , "42" _
                       , "|3" _
                       , ResolveResString(7) _
                       )

This will produce the following message box:

Keeping message text in a resource file keeps strings (which could include SQL strings) neatly together in one place, and also flags them as discardable data, stuff that Windows can throw away if it must. Don't worry about this—Windows can reload strings from your binary image if it needs them. Your code is treated in exactly the same way and you've never worried about that being discarded, have you? Keeping read-only data together like this allows Visual Basic and Windows to better optimize how they use memory. Resource files also provide something akin to reuse as they usually allow you to be "cleverer" in your building of SQL and error text—they might even provide a way for you to share data like this across several applications and components. (See the notes on using a ROOS earlier in this chapter.)


Tip 22: Always handle errors in controls and components (that you build).

Errors in controls

When a control raises an unhandled error (by the control), the error is reported and the control becomes disabled—it actually appears hatched—or the application terminates. (See Figure 1-7 and Figure 1-8.)

Figure 1-7 Containing form before the error

Figure 1-8 Containing form after the error

It's important to know that errors in a UserControl can be propagated to two different levels. If the errors are caused wholly by the control, they will be handled by the control only. If the errors are instigated via a call to an external interface on the control, from the containing application, they will be handled by the container. Another way to state this is to say that whatever is at the top of the call stack will handle unhandled errors. If you call into a control, say from a menu selection in the container, the first entry in your call stack will be the container's code. That's where the mnuWhatever_Click occurred. If the control raises an error now, the call stack is searched for a handler, all the way to the top. In this case, any unhandled control error has to be handled in the container, and if you don't handle it there, you're dead when the container stops and, ergo, so does the control. However, if the control has its own UI or maybe a button, your top-level event could be a Whatever_Click generated on the control itself. The top of your call stack is now your control code and any unhandled errors cause only the control to die. The container survives, albeit with a weird-looking control on it. (See Figure 1-8.)

This means that you must fragment your error handling across containers and controls, not an optimal option. Or you need some way of raising the error on the container even if the container's code isn't on the stack at the moment the error occurs. A sort of Container.Err.Raise thing is required.

In each of our container applications (those applications that contain UserControls), we have a class called ControlErrors (usually one instance only). This class has a bundle of miscellaneous code in it that I won't cover here, and a method that looks something like this:

Public Sub Raise(ParamArray v() As Variant)

On Error GoTo ControlErrorHandler:

    ' Basically turns a notification into an error -
    ' one easy way to populate the Err object.
    Err.Raise v(0), v(1), v(2), v(3), v(4)

    Exit Sub

ControlErrorHandler:

    MsgBox "An error " & Err.Number & " occurred in " & Err.Source & _
           " UserControl.  The error is described as " & Err.Description

End Sub

In each container application we declare a new instance of ControlErrors, and for each of our UserControls we do what's shown below.

If True = UserControl1.UsesControlErrors Then

    Set UserControl1.ErrObject = o

End If

UsesControlErrors returns True if the UserControl has been written to "know" about a ControlErrors object.

In each control—to complete the picture—we have something like this (UsesControlErrors is not shown):

Private ContainerControlErrors As Object

Private Sub SomeUIWidget_Click()

On Error GoTo ErrorHandler:

    Err.Raise ErrorValue

    Exit Sub

ErrorHandler:

    ' Handle top-level event error.

    ' Report error higher up?
    If Not ContainerControlErrors Is Nothing Then

        ContainerControlErrors.Raise ErrorValue

    End If

End Sub

Public Property Set ErrObject(ByVal o As Object)

    Set ContainerControlErrors = o

End Property

We know from this context that SomeUIWidget_Click is a top-level event handler (so we must handle errors here), and we can make a choice as to whether we handle the error locally or pass it on up the call chain. Of course, we can't issue a Resume Next from the container once we've handled the (reporting of the) error—that's normal Visual Basic. But we do at least have a mechanism whereby we can report errors to container code, perhaps signalling that we (the control) are about to perform a Resume Next or whatever.

Errors in OLE servers

Raising errors in a Visual Basic OLE Automation server is much the same as for a stand-alone application. However, some consideration must be given to the fact that your server may not be running in an environment in which errors will be visible to the user. For example, it may be running as a service on a remote machine. In these cases, consider these two points:

  1. Don't display any error messages. If the component is running on a remote machine, or as a service with no user logged on, the user will not see the error message. This will cause the client application to lock up because the error cannot be acknowledged.

  2. Trap every error in every procedure. If Visual Basic's default error handler were executed in a remote server, and assuming you could acknowledge the resulting message box, the result would be the death of your object. This would cause an Automation error to be generated in the client on the line where the object's method or property was invoked. Because the object has now died, you will have a reference in your client to a nonexistent object.

To handle errors in server components, first trap and log the error at the source. In each procedure, ensure that you have an Err.Raise to guarantee that the error is passed back up the call stack. When the error is raised within the top-level procedure, the error will propagate to the client. This will leave your object in a tidy state; indeed, you may continue to use the same object.

If you are raising a user-defined error within your component you should add the constant vbObjectError (&H80040000&). Using vbObjectError causes the error to be reported as an Automation error. To extract the user-defined error number, subtract vbObjectError from Err.Number. Do not use vbObjectError with Visual Basic-defined errors; otherwise, an "Invalid procedure call" error will be generated.


Tip 23: Use symbolic debugging information.

For extra tricky debugging, you should check out the Visual Studio debugger (which you get to by starting Visual C++). You obviously need the whole thing, in Visual Studio terms, to use this debugger in Visual Basic (or have a third-party debugger that can use the symbolic debugging information produced by Visual Basic). You'll also need some instructions on using the debugger, as it's one of the least documented features that I've ever seen.

To use the Visual Studio debugger, if you have it, do the following.

Build your Visual Basic application using the Compile To Native Code option. On the Compile tab of the Project Properties dialog box, select the Create Symbolic Debug Info and No Optimization options. Make sure that you build the EXE/DLL/OCX (I'll assume you're debugging a simple EXE from here on in) so that the binary is stored in the same folder as the source code. Start Visual C++, select Open from the File menu, and then select your built EXE file. Select Open from the File menu again (more than once if necessary) to select the source files you want to debug and trace through (FRM, BAS, CLS, etc.). Move the cursor to the line(s) where you want to start your debugging, and hit F9 to set a breakpoint. Once you've done this, hit F5. If you've set a breakpoint on, say, a Form_Load that runs as the program starts, you should immediately have broken to the debugger at this point. One more thing—use F10 to step through your code, not F8.

See your Visual Studio documentation (and Chapter 7) for more on how to use the debugger.

More on Client/Server Error Handling

Components should be nonintrusive about their errors. Instead of raising their own errors in message boxes, or through other UI, directly to the user, the components should pass any error that cannot be handled and corrected entirely within the component to their client's code, where the developer can decide how to handle it.

It is possible and even highly likely that a component you're using is itself a client of other components. What should you do when your component gets an error from one that it's using? Well, if you can't recover from the error, you should encapsulate it. It's bad practice merely to raise the other component's error to your client, since your client may well know nothing about this other component and is just dealing with your component. This means that you need to provide meaningful errors from your own component. Rather than passing up other component's errors or Visual Basic errors to your client, you need to define your own and use these. Public constants or Enums with good names are an excellent way of doing this, since they give a source for all errors in your component, and also should give strong clues about each error in its name.

When defining your own error numbers in components, remember that you should use vbObjectError and that currently Microsoft recommends keeping numbers added to it in a range between 512 and 65535.

Constant vbBaseErr        As long = 512
Constant ComponentBaseErr As long = vbObjectError + vbBaseErr
Constant MaxErr           As long = vbObjectError + 65535

Remember that there are two occasions when you can safely raise an error in Visual Basic:

  1. When error handling is turned off

  2. When in a procedure's error handler

It is possible to raise events to pass errors to clients in some circumstances. The standard Visual Basic Data control has an error event, for instance.

Logging errors and tracing procedure stacks in components raises some special problems. It clearly makes sense to log errors, which are sent back to a client from a component. Thus, if you're creating an OCX, you would raise an error and expect that the client code would log that error in its error log. Components on remote machines may also have specific requirements about where to log.

For components there are a number of possible error log locations:

  • Files (either flat files or Structured OLE storages)

  • NT Event Log (but beware of using this with components deployed in Microsoft Transaction Server)

  • Databases (but always have some other means to fall back on if your database connections fail)

It makes sense to add other information to a component's error log, because it's useful to know the UserID or Logon name, the machine name, the process ID, and the thread ID. We frequently use a class for this purpose, which returns the following:

  • Process ID

  • Thread ID

  • Machine name

  • Network version info

  • LanGroup

  • LanRoot

  • Current user name

  • Logon server

  • Domain

  • Number of users currently logged on

  • Other available domains

This information is useful for sorting information in a component's error log.


Chapter 2 -- Taking Care of Business (Objects)

Chapter 2

Taking Care of Business (Objects)

ADAM MAGEE

Adam Magee is a software developer who specializes in building enterprise applications. He has worked with many large companies in the United Kingdom and in Australia, helping them to implement tiered architecture solutions. His particular focus is on the design and process of building efficient, high performance, distributable business objects. Some developers would like to see Adam in a deep gully position but he prefers it at backward square leg.

Taking care of business every day
Taking care of business every way
I've been taking care of business, it's all mine
Taking care of business and working overtime
Work out!

"Taking Care of Business"

by Bachmann Turner Overdrive

Business objects are big news. Everyone says the key to building distributed enterprise applications is business objects. Lots of 'em.

Business objects are cool and funky—we need business objects! You need business objects! Everyone needs business objects!

This chapter presents a design pattern for developing solid, scalable, robust business objects, designed for (and from) real-life distributed enterprise applications.

The architecture we propose at The Mandelbrot Set (International) Limited (TMS) for building distributed business object-based applications breaks down into various layers of functionality.

The data access layer (DAL) is the layer that talks to the data source. The interface of this layer should be the same regardless of the type of data source being accessed.

The business object layer is the actual layer that models the entities within your application—the real data, such as Employees, Projects, Departments, and so forth. In Unified Modeling Language (UML) parlance, these are called entity objects.

The action object layer represents the processes that occur on the business objects: these processes are determined by the application requirements. In UML, these action object layers are called control objects.

The client application layer, such as a Microsoft Visual Basic form, an ActiveX document, or an Active Server Page (ASP) on a Web server, is known as an interface object in UML.

This chapter concentrates on the middle business object layer and introduces the concepts of action objects, factory objects, and worker objects.


Data Access Layer

The most common question I get asked by Visual Basic developers is, "What type of DAL should I use? Data Access Objects (DAO), Open Database Connectivity (ODBC) API, ODBCDirect, Remote Data Objects (RDO), ActiveX Data Objects (ADO), VBSQL, vendor-specific library, smoke signals, semaphores, Morse code, underwater subsonic communications?" The answer is never easy. (Although in windier climates I would advise against smoke signals.)

What I do think is a good approach to data access is to create a simple abstract data interface for your applications. This way, you can change your data access method without affecting the other components in your application. Also, a simple data interface means that developers have a much lower learning curve and can become productive more quickly.

Remember that we are trying to create a design pattern for business object development in corporate database-centric applications. These applications typically either send or receive data or they request a particular action to be performed; as such, a simple, straightforward approach is required. I like simplicity—it leads to high-quality code.

So am I saying don't use ADO? No, not at all. Most Microsoft-based solutions are well suited to using ADO inside the DAL. But by concentrating your ADO code (or any database connection library for that matter) into one component, you achieve a more maintainable, cohesive approach. Besides, ADO is still a complex beast. If you take into account that many of ADO's features are restricted by the type of database source (such as handling text fields and locking methodologies), you'll find that the benefits of a stable, easily understood DAL far outweigh the cost of slightly reinventing the wheel.

Not only is abstracting the data access methodology important, but abstracting the input and output from this component are important as well. The DAL should return the same data construct regardless of the database source.

But enough theoretical waffling—let's look at something real.

Data Access Layer Specifics

The DAL presented here is essentially the same DAL that TMS uses in enterprise applications for its clients. I'll describe the operation of the DAL in detail here, but I'll concentrate only on using a DAL, not on building one. Remember that we want to concentrate on business object development, so this is the data object that our business objects will interface with.

The DAL below consists of only six methods. All other aspects of database operation (for instance, cursor types, locking models, parameter information, and type translation) are encapsulated and managed by the DAL itself.

cDAL Interface
Member | Description
OpenRecordset | Returns a recordset of data
UpdateRecordset | Updates a recordset of data
ExecuteCommand | Executes a command that doesn't involve a recordset
BeginTransaction | Starts a transaction
CommitTransaction | Commits a transaction
RollbackTransaction | Rolls back a transaction

What's all this recordset malarkey? Well, this is (surprise, surprise) an abstracted custom recordset (cRecordset). We must investigate this recordset carefully before we look at the operation of the DAL itself.

Recordset

The method TMS uses for sending and receiving data to and from the database is a custom recordset object (cRecordset). In this chapter, whenever I mention recordset, I am referring to this custom cRecordset implementation, rather than to the various DAO/RDO/ADO incarnations.

A recordset is returned from the DAL by calling the OpenRecordset method. This recordset is completely disconnected from the data source. Updating a recordset will have no effect on the central data source until you call the UpdateRecordset method on the DAL.

We could have easily returned an array of data from our DAL, especially since ADO and RDO nicely support GetRows, which does exactly that, but arrays are limited in a number of ways.

Array manipulation is horrible. In Visual Basic, you can resize only the last dimension of an array, so forget about adding columns easily. Also, arrays are not self-documenting. Retrieving information from an array means relying on such hideous devices as constants for field names and the associated constant maintenance, or the low-performance method of looping through indexes looking for fields.

Enabling varying rows and columns involves using a data structure known as a ragged array—essentially an array of arrays—which can be cumbersome and counterintuitive to develop against.

The advantage of using a custom recordset object is that we can present the data in a way that is familiar to most programmers, but we also get full control of what is happening inside the recordset. We can again simplify and customize its operation to support the rest of our components.

Notice the Serialize method, which allows us to move these objects easily across machine boundaries. More on this powerful method later. For the moment, let's look at the typical interface of a cRecordset.

cRecordset Interface

Member | Description
MoveFirst | Moves to first record
MoveNext | Moves to next record
MovePrevious | Moves to previous record
MoveLast | Moves to last record
Name | Shows the name of the recordset
Fields | Returns a Field object
Synchronize | Refreshes the contents of the recordset
RowStatus | Shows whether this row has been created, updated, or deleted
RowCount | Shows the number of records in the recordset
AbsolutePosition | Shows the current position in the recordset
Edit | Copies the current row to a buffer for modification
AddNew | Creates an empty row in a buffer for modification
Update | Commits the modification in the buffer to the recordset
Serialize | Converts or sets the contents of the recordset to an array

This table shows the details of the interface for the cField object, which is returned from the cRecordset object.

cField Interface

Member | Description
Name | Name of the field
Value | Contents of the field
Type | Visual Basic data type
Length | Length of the field

Retrieving a Recordset

So how can we utilize this cRecordset with our DAL? Here's an example of retrieving a recordset from the DAL and displaying information:

Dim oDAL     As New cDAL
Dim oRec     As New cRecordset

Set oRec = oDAL.OpenRecordset("Employees")
oRec.MoveFirst
MsgBox oRec.Fields("FirstName").Value

Please forgive me for being a bit naughty in using automatically instantiated object variables, but this has been done purely for code readability.

Notice that the details involved in setting up the data connection and finding the data source are all abstracted by the DAL. Internally, the DAL is determining where the Employees data lives and is retrieving the data and creating a custom recordset. In a single-system DAL, the location of the data could be assumed to be in a stored procedure on a Microsoft SQL Server; in a multidata source system, the DAL might be keying into a local database to determine the location and type of the Employees data source. The implementation is dependent upon the requirements of the particular environment.

Also, the operation in the DAL is stateless. After retrieving the recordset, the DAL can be closed down and the recordset can survive independently. This operation is critical when considering moving these components to a hosted environment, such as Microsoft Transaction Server (MTS).

Statelessness is important because it determines whether components will scale. The term scale means that the performance of this object will not degrade when usage of the object is increased. An example of scaling might be moving from two or three users of this object to two or three hundred. A stateless object essentially contains no module-level variables. Each method call is independent and does not rely on any previous operation, such as setting properties or other method calls. Because the object has no internal state to maintain, the same copy of the object can be reused for many clients. There is no need for each client to have a unique instance of the object, which also allows products such as Microsoft Transaction Server to provide high-performance caching of these stateless components.

Serializing a Recordset

Two of the primary reasons for employing a custom recordset are serialization and software locking. Because passing objects across machines causes a considerable performance penalty, we need a way of efficiently moving a recordset from one physical tier to another.

Serialization allows you to export the contents of your objects (such as the variables) as a primitive data type. What can you do with this primitive data type? Well, you can use it to re-create that object in another environment—maybe in another process, maybe in another machine. All you need is the class for the object you have serialized to support the repopulation of its internal variables from this primitive data type. The process of serialization has tremendous performance advantages in that we can completely transfer an object to another machine and then utilize the object natively in that environment without incurring the tremendous performance cost that is inherent in accessing objects across machine boundaries.

The cRecordset object stores its data and state internally in four arrays. The Serialize property supports exposing these arrays to and receiving them from the outside world, so transferring a recordset from one physical tier to another is simply a matter of using the Serialize property on the cRecordset. Here's an example:

Dim oRec     As New cRecordset
Dim oDAL     As New cDAL  ' Assume this DAL is on another machine.

oRec.Serialize = oDAL.OpenRecordset("Employees").Serialize
Set oDAL = Nothing
MsgBox oRec.RecordCount

Now we have a recordset that can live independently. It can even be passed to another machine and then updated by a DAL on that machine, if required.

Locking a Recordset

We need to keep track of whether the data on the central data source has changed since we made our copy. This is the job of one of the arrays inside the cRecordset, known affectionately as the CRUD array.

The CRUD array indicates whether this row has been Created, Updated, Deleted, or just plain old Read. Also stored is a globally unique identifier (GUID) for this particular row. This unique identifier must be automatically updated when a row is modified on the data source. These two parameters are used by the DAL in the UpdateRecordset method to determine whether a row needs updating and whether this row has been modified by someone since the client received the recordset. This process is a form of software locking, although it could be internally implemented just as easily using timestamps (if a given data source supports them).

Updating a Recordset

Updating a cRecordset object through the DAL occurs by way of the UpdateRecordset method. UpdateRecordset will scan through the internal arrays in the recordset and perform the required database operation. The unique row identifier is used to retrieve each row for modification, so if someone has updated a row while the recordset has been in operation this row will not be found and an error will be raised to alert the developer to this fact. The following is an example of updating a row in a recordset and persisting that change to the data source:

Dim oRec     As New cRecordset
Dim oDAL
    As New cDAL  ' Assume local DAL so don't need serialization

Set oRec = oDAL.OpenRecordset("Employees")
oRec.Edit
oRec.Fields("FirstName") = "Adam Magee"
oRec.Update
oDAL.UpdateRecordset oRec

Synchronizing a Recordset

After a cRecordset object has been used by UpdateRecordset to successfully update the data source, the cRecordset object needs to have the same changes committed to itself, which is accomplished by means of the Synchronize method.

Using the Synchronize method will remove all Deleted rows from the recordset and will set any Updated or Created rows to Read status. This gives the developer using the cRecordset object control over committing changes and also means the recordset state can be maintained in the event of an UpdateRecordset failure. Here is an example of synchronizing a recordset after a row has been updated:

oDAL.UpdateRecordset oRec
oRec.Synchronize
Parameters

Simply supplying the name of a cRecordset object to OpenRecordset is usually not enough information, except maybe when retrieving entire sets of data, so we need a way of supplying parameters to a DAL cRecordset operation.

This is achieved by using a cParams object. The cParams object is simply a collection of cParam objects, which have a name and a value. The cParams object, like the cRecordset object, can also be serialized. This is useful if the parameters need to be maintained on another machine.

CParams Interface

Member | Description
Add | Adds a new cParam object with a name and a value
Item | Returns a cParam object
Count | Returns the count of collected cParam objects
Remove | Removes a cParam object
Serialize | Converts or sets the content of cParams object into an array

CParam Interface

Member | Description
Name | Name of the parameter
Value | Variant value of the parameter

Here is an example of retrieving a recordset with parameters:

Dim oDAL As New cDAL
Dim oRec As New cRecordset
Dim oPar As New cParams

oPar.AddField "EmpID", "673"
oPar.AddField "CurrentlyEmployed", "Yes"

Set oRec = oDAL.OpenRecordset("Employees", oPar)

MsgBox oRec.RecordCount

We can see now that only two well-defined objects are parameters to the DAL—cRecordset and cParams—and both of these objects support serialization, giving a consistent, distributed operation-aware interface.

Commands

A lot of database operations do not involve, or should not involve, cRecordset objects. For instance, checking and changing a password are two operations that do not require the overhead of instantiating and maintaining a cRecordset. This is where you use the ExecuteCommand method of the DAL. The ExecuteCommand method takes as parameters both the name of the command to perform and a cParams object.

Any output cParam objects generated by the command are automatically populated into the cParams object if they are not supplied by default. Here's an example of checking a password:

Dim oDAL As New cDAL
Dim oPar As New cParams

oPar.Add "UserID", "637"
oPar.Add "Password", "Chebs"
oPar.Add "PasswordValid", ""     ' The DAL would have added this output
                                 ' parameter if we had left it out.
oDAL.ExecuteCommand "CheckPassword", oPar

If oPar("PasswordValid").Value = "False" Then
    Msgbox "Sorry Invalid Password", vbExclamation
End If

Transactions

Most corporate data sources support transactions; our DAL must enable this functionality as well. This is relatively easy if you are using data libraries such as DAO or RDO, since it is a trivial task to simply map these transactions onto the underlying calls. If your data source does not support transactions, you might have to implement this functionality yourself. If so, may the force be with you. The three transaction commands are BeginTransaction, CommitTransaction, and RollbackTransaction.

The DAL is taking care of all the specifics for our transaction, leaving us to concentrate on the manipulation code. In the case of a transaction that must occur across business objects, we'll see later how these business objects will all support the same DAL. Here's an example of updating a field inside a transaction:

Dim oRec As New cRecordset
Dim oDAL As New cDAL  ' Assume local DAL so don't need serialization

With oDAL
    .BeginTransaction
    Set oRec = oDAL.OpenRecordset("Employees")
    With oRec
        .Edit
        .Fields("FirstName") = "Steve Gray"
        .Update
    End With
    .UpdateRecordset oRec
    .CommitTransaction
End With
Wrap Up

So that's a look at how our abstracted data interface works. I hope you can see how the combination of cDAL, cRecordset, and cParams presents a consistent logical interface to any particular type of data source. There is comprehensive support for distributed operation in the sense that the DAL is completely stateless and that the input and output objects (cParams and cRecordset) both support serialization.


Factory-Worker Objects

So what, really, is a business object and how is it implemented? Well, in the Visual Basic world, a business object is a public object that exposes business-specific attributes. The approach TMS takes toward business objects is to employ the action-factory-worker model. We'll come to the action objects later, but for now we'll concentrate on the factory-worker objects.

A quick note about terminology: in this design pattern there is no such thing as an atomic "business object" itself. The combination of the interaction between action, worker, and factory can be described as a logical business object.

Factory-Worker Model

The factory-worker model stipulates that for each particular type of business object an associated management class will exist. The purpose of the management class is to control the creation and population of data in the business objects. This management class is referred to as a factory class. The interface of every factory class is identical (except under exceptional circumstances).

Likewise, the worker class is the actual business object. Business objects cannot be instantiated without an associated factory class. In Visual Basic-speak we say that they are "Public Not Creatable"—the only way to gain access to a worker object is through the publicly instantiable/creatable factory class. So when we refer to a business object we are actually talking about the combination of the factory and worker objects, since each is dependent on the other.

Shared recordset implementation

Adding all this factory-worker code to your application isn't going to make it any faster. If your worker objects had 30 properties each and you wanted to create 1000 worker objects, the factory class would have to receive 1000 rows from the database and then populate each worker with the 30 fields. This would require 30,000 data operations! Needless to say, a substantial overhead.

What you need is a method of populating the worker objects in a high-performance fashion. The Shared Recordset Model solves this problem, which means that one cRecordset is retrieved from the DAL and each worker object is given a reference to a particular row in that recordset. This way, when a property is accessed on the worker object, the object retrieves the data from the recordset rather than from explicit internal variables, saving us the overhead of populating each worker object with its own data.

Populating each worker object involves instantiating only the object and then passing an object reference to the Shared Recordset and a row identifier, rather than setting all properties in the worker object individually. The worker object uses this object reference to the recordset to retrieve or write data from its particular row when a property of the business object is accessed. But to establish the real benefits of using the factory-worker approach, we need to discuss briefly how distributed clients interface with our business objects. This is covered in much greater detail in the sections, "Action Objects" and "Clients," later in this chapter.

To make a long story short, distributed clients send and receive recordsets only. Distributed clients have no direct interface to the business objects themselves. This is the role of action objects. Action objects act as the brokers between client applications and business objects. The recordset supports serialization, so the clients use this "serializable" recordset as a means of transferring data to and from the client tier to the business tier via the action object.

It's quite common for a client to request information that originates from a single business object. Say, for example, that the client requests all the information about an organization's employees. What the client application wants to receive is a recordset containing all the Employee Detail information from an action object. The EmployeeDetail recordset contains 1800 employees with 30 fields of data for each row.

Let's look at what's involved in transferring this information from the business objects to the client if we don't use the Shared Recordset implementation.

  1. The client requests the EmployeeDetail recordset from the Employee Maintenance action object.

  2. The Employee Maintenance action object creates an Employee factory object.

  3. The Employee factory object obtains an Employee recordset from the DAL.

  4. The Employee factory object creates an Employee worker object for each of the rows in the recordset.

  5. The Employee factory object sets the corresponding property on the Employee worker object for each of the fields in that row of the recordset.

    We now have a factory-worker object containing information for our 1800 employees. But the client needs all this information in a recordset, so the client follows these steps:

  6. The Employee Maintenance action object creates a recordset.

  7. The Employee Maintenance action object retrieves each Employee worker object from the Employee factory object and creates a row in the recordset.

  8. Each property on that Employee worker object is copied into a field in the recordset.

  9. This recordset is returned to the client and serialized on the client side.

  10. The client releases the reference to the action object.

Basically, the business object gets a recordset, tears it apart, and then the action object re-creates exactly the same recordset we had in the first place. In this case, we had 1800 × 30 data items that were set and then retrieved, for a total of 108,000 data operations performed on the recordsets!

Let's look at the difference if we use the Shared Recordset Model.

  1. The client requests an EmployeeDetail recordset from the Employee Maintenance action object.

  2. The Employee Maintenance action object creates an Employee factory object.

  3. The Employee factory object obtains an Employee recordset from the DAL.

  4. The Employee factory object keeps a reference to the recordset.

    NOTE

    Notice that at this point the factory will not create any objects; rather, it will create the objects only the first time they are requested—in essence, it will create a Just In Time (JIT) object.

  5. The Employee Maintenance action object obtains a reference to the recordset from the Employee factory object via the Recordset property.

  6. This recordset is returned to the client and serialized on the client side.

  7. The client releases the reference to the action object.

Total data operations on the recordset—zero. We can now return large sets of data from a business object in a high-performance fashion. But this leads to the question of why you should bother with the business objects at all. If all the client is doing is requesting a recordset from the DAL, why the heck doesn't it just use the DAL directly?

Well, to do so would completely ignore the compelling arguments for object-oriented programming. We want contained, self-documenting abstracted objects to represent our core business, and this scenario is only receiving data, not performing any methods on these objects.

Remember that this is a particular case when a client, on a separate physical tier, requests a set of data that directly correlates to a single business object. The data will often need to be aggregated from one or more business objects. So the action object, which exists on the same physical tier as the business object, will have full access to the direct worker object properties to perform this data packaging role.

A client will often request that a complex set of business logic be performed. The action object will perform this logic by dealing directly with the worker objects and the explicit interfaces they provide. Thus, the action objects can fully exploit the power of using the objects directly.

Using the worker objects directly on the business tier means we are creating much more readable, self-documenting code. But because of the advantages of the Shared Recordset implementation, we are not creating a problem in terms of performance. If we need to shift massive amounts of data to the client, we can still do it and maintain a true business object approach at the same time—we have the best of both worlds.

Now that we have covered the general architecture of the action-factory-worker-recordset interaction, we can take a closer look at the code inside the factory and worker objects that makes all this interaction possible.

Factory Objects

The interface for the factory object is as follows:

cFactory Interface
Member | Description
Create | Initializes the business object with a DAL object
Populate | Creates worker objects according to any passed-in parameter object
Item | Returns a worker object
Count | Returns the number of worker objects contained in the factory
Add | Adds an existing worker object to the factory
AddNew | Returns a new empty worker object
Persist | Updates all changes in the worker objects to the database
Remove | Removes a worker object from the factory
Recordset | Returns the internal factory recordset
Delete | Deletes a worker object from the data source and the factory
Parameters | Returns the parameters that can be used to create worker objects

Creating a factory object

Creating the factory is simple. The code below demonstrates the use of the Create method to instantiate a factory object. This code exists in the action objects (which we'll cover later). This sample action object uses the Employees business object to perform a business process.

Dim ocDAL As New cDAL
Dim ocfEmployees As New cfEmployees
ocfEmployees.Create ocDAL

Hold it—what's the DAL doing here? Isn't the purpose of business objects to remove the dependency on the DAL? Yes, it is. But something important is happening here, and it has everything to do with transactions. The scope and lifetime of the action object determines the scope and lifetime of our transaction for the business objects as well.

Say our action object needs to access four different factory objects and change data in each of them. Somehow each business object needs to be contained in the same transaction. This is achieved by having the action object instantiate the DAL, activating the transaction and passing that DAL to all four factory objects it creates. This way, all our worker object activities inside the action object can be safely contained in a transaction, if required. More on action objects and transactions later.

So what does the Create code look like? Too easy:

Public Sub Create(i_ocDAL As cDAL)
    Set oPicDAL = i_ocDAL ' Set a module-level reference to the DAL.
End Sub

Populating a factory object

Creating a factory object isn't really that exciting—the fun part is populating this factory with worker objects, or at least looking like we're doing so!

Dim ocDAL           As New cDAL
Dim ofcEmployees    As New cfEmployees
Dim ocParams        As New cParams
ofcEmployees.Create oDAL
ocParams.Add "Department", "Engineering"
ofcEmployees.Populate ocParams

Here a recordset has been defined in the DAL as Employees, which can take the parameter Department to retrieve all employees for a particular department. Good old cParams is used to send parameters to the factory object, just like it does with the DAL. What a chipper little class it is!

So there you have it—the factory object now contains all the worker objects ready for us to use. But how does this Populate method work?

Private oPicRecordset As cRecordset
Public Sub Populate (Optional i_ocParams as cParams)
    Set oPicRecordset = oPicDAL.OpenRecordset("Employees", i_ocParams)
End Sub

The important point here is that the Populate method is only retrieving the recordset—it is not creating the worker objects. Creating the worker objects is left for when the user accesses the worker objects via either the Item or Count method.

Some readers might argue that using cParams instead of explicit parameters detracts from the design. The downside of using cParams is that the parameters cannot be determined for this class at design time and do not contribute to the self-documenting properties of components. In a way I agree, but using explicit parameters also has its limitations.

The reason I tend to use cParams rather than explicit parameters in the factory object Populate method is that the interface to the factory class is inherently stable. With cParams all factory objects have the same interface, so if parameters for the underlying data source change (as we all know they do in the real world) the public interface of our components will not be affected, thereby limiting the dreaded Visual Basic nightmare of incompatible components.

Also of interest in the Populate method is that the cParams object is optional. A Populate method that happens without a set of cParams is determined to be the default Populate method and in most cases will retrieve all appropriate objects for that factory. This functionality is implemented in the DAL.

Obtaining a worker object

After we have populated the factory object, we can retrieve worker objects via the Item method as shown here:

Dim ocDAL            As New cDAL
Dim ofcEmployees     As New cfEmployees
Dim owcEmployee      As New cwEmployee
Dim ocParams         As New cParams

ofcEmployees.Create ocDAL
ocParams.Add "Department", "Engineering"
ofcEmployees.Populate ocParams
Set owcEmployee = ofcEmployees.Item("EdmundsM")
MsgBox owcEmployee.Department

At this point, the Item method will initiate the instantiation of worker objects. (Nothing like a bit of instantiation initiation.)

Public Property Get Item(i_vKey As Variant) As cwEmployee
    If colPicwEmployee Is Nothing Then PiCreateWorkerObjects
    Set Item = colPicwEmployee(i_vKey).Item
End Property

So what does PiCreateWorkerObjects do?

Private Sub PiCreateWorkerObjects
    Dim owcEmployee As cwEmployee

    Do Until oPicRecordset.EOF
        Set owcEmployee = New cwEmployee
        owcEmployee.Create oPicRecordset, oPicDAL
        oPicRecordset.MoveNext
    Loop
End Sub

Here we can see the payback in performance for using the Shared Recordset Model. Initializing the worker object simply involves calling the Create method of the worker and passing in a reference to oPicRecordset and oPicDAL. The receiving worker object will store the current row reference and use this to retrieve its data.

But why is the DAL reference there? The DAL reference is needed so that a worker object has the ability to create a factory of its own. This is the way object model hierarchies are built up. (More on this later.)

The Item method is also the default method of the class, enabling us to use the coding-friendly syntax of

ocfEmployees("637").Name

Counting the worker objects

Couldn't be simpler:

MsgBox CStr(ofcEmployees.Count)

Private Property Get Count() As Long
    Count = colPicfEmployees.Count
End Property

Says it all, really.

Adding workers to factories

Often you will have a factory object to which you would like to add pre-existing worker objects. You can achieve this by using the Add method. Sounds simple, but there are some subtle implications when using the Shared Recordset implementation. Here it is in action:

Dim ocDAL            As New cDAL
Dim ocfEmployees     As New cfEmployees
Dim ocwEmployee      As New cwEmployee

ocfEmployees.Create ocDAL
Set ocwEmployee = MagicEmployeeCreationFunction()
ocfEmployees.Add ocwEmployee

You'll run into a few interesting quirks when adding another object. First, since the worker object we're adding to our factory has its data stored in another factory somewhere, we need to create a new row in our factory's recordset and copy the data from the worker object into the new row. Then we need to set this new object's recordset reference from its old parent factory to the new parent factory, otherwise it would be living in one factory but referencing data in another—that would be very bad. To set the new reference, we must call the worker Create method to "bed" it into its new home.

Public Sub Add(i_ocwEmployee As cwEmployee)
    oPicRecordset.AddNew
    With oPicRecordset.Fields
        .("ID") = i_ocwEmployee.ID
        .("Department") = i_ocwEmployee.Department
        .("FirstName") = i_ocwEmployee.FirstName
        .("LastName") = i_ocwEmployee.LastName
    End With
    oPicRecordset.Update
    i ocwEmployee.Create oPicRecordset
End Sub

And there you have it—one worker object in a new factory.

Creating new workers

You use the AddNew method when you want to request a new worker object from the factory. In the factory, this involves adding a row to the recordset and creating a new worker object that references this added row. One minor complication here: what if I don't have already have a recordset?

Suppose that I've created a factory object, but I haven't populated it. In this case, I don't have a recordset at all, so before I can create the new worker object I have to create the recordset. Now, when I get a recordset from the DAL, it comes back already containing the required fields. But if I don't have a recordset, I'm going to have to build one manually. This is a slightly laborious task because it means performing AddField operations for each property on the worker object. Another way to do this would be to retrieve an empty recordset from the DAL, either by requesting the required recordset with parameters that will definitely return an empty recordset, or by having an optional parameter on the OpenRecordset call. In our current implementation, however, we build empty recordsets inside the factory object itself.

But before we look at the process of creating a recordset manually, let's see the AddNew procedure in action:

Dim ocDAL           As New cDAL
Dim ocfEmployees    As New cfEmployees
Dim ocwEmployee     As New cwEmployee

ocfEmployees.Create ocDAL
Set ocfEmployee = ocfEmployees.AddNew
ocfEmployee.Name = "Adam Magee"

This is how it is implemented:

Public Function AddNew() As cwEmployee
    Dim ocwEmployee As cwEmployee

    If oPicRecordset Is Nothing Then
        Set oPicRecordset = New cRecordset
        With oPicRecordset
            .AddField "ID"
            .AddField "Department"
            .AddField "FirstName"
            .AddField "LastName"
        End With
    End If
    oPicRecordset.AddNew    ' Add an empty row for the
                            ' worker object to reference.
    oPicRecordset.Update
    Set ocwEmployee = New cwEmployee
    ocwEmployee.Create oPicRecordset, oPicDAL
    colPicwWorkers.Add ocwEmployee
    Set New = ocwEmployee
End Property

This introduces an unavoidable maintenance problem, though. Changes to the worker object must now involve updating this code as well—not the most elegant solution, but it's worth keeping this in mind whenever changes to the worker objects are implemented.

Persistence (Perhaps?)

So now we can retrieve worker objects from the database, we can add them to other factories, and we can create new ones—all well and good—but what about saving them back to the data source? This is the role of persistence. Basically, persistence involves sending the recordset back to the database to be updated. The DAL has a method that does exactly that—UpdateRecordset—and we can also supply and retrieve any parameters that might be appropriate for the update operation (although most of the time UpdateRecordset tends to happen without any reliance on parameters at all).

Dim ocDAL            As New cDAL
Dim ocfEmployees     As New cfEmployees
Dim ocwEmployee      As New cwEmployee
Dim ocParams         As New cParams

ocfEmployees.Create ocDAL
ocParams.Add "Department", "Engineering"
ocfEmployees.Populate ocParams
For Each ocwEmployee In ocfEmployees
    .Salary = "Peanuts"
Next  'ocwEmployee
ocfEmployees.Persist     ' Reduce costs

What is this Persist method doing, then?

Public Sub Persist(Optional i_ocParams As cParams)
    oPicDAL.UpdateRecordset oPicRecordset, i_ocParams
End Sub

Consider it persisted.

Removing and Deleting

What about removing worker objects from factories? Well, you have two options—sack 'em or whack 'em!

A worker object can be removed from the factory, which has no effect on the underlying data source. Maybe the factory is acting as a temporary collection for worker objects while they wait for an operation to be performed on them. For example, a collection of Employee objects needs to have the IncreaseSalary method called (yeah, I know what you're thinking—that would be a pretty small collection). For one reason or another, you need to remove an Employee worker object from this factory (maybe the worker object had the SpendAllDayAtWorkSurfingTheWeb property set to True), so you would call the Remove method. This method just removes the worker object from this factory with no effect on the underlying data. This is the sack 'em approach.

You use the other method when you want to permanently delete an object from the underlying data source as well as from the factory. This involves calling the Delete method on the factory and is known as the whack 'em approach. Calling Delete on the factory will completely remove the worker object and mark its row in the database for deletion the next time a Persist is executed.

This is an important point worth repeating—if you delete an object from a factory, it is not automatically deleted from the data source. So if you want to be sure that your worker objects data is deleted promptly and permanently, make sure that you call the Persist method! Some of you might ask, "Well, why not call the Persist directly from within the Delete method?" You wouldn't do this because of performance. If you wanted to delete 1000 objects, say, you wouldn't want a database update operation to be called for each one—you would want it to be called only at the end when all objects have been logically deleted.

Dim ocDAL            As New cDAL
Dim ocfEmployees     As New cfEmployees
Dim ocwEmployee      As New cwEmployee

ocfEmployees.Create ocDAL
ocfEmployees.Populate
For Each ocwEmployee In ocfEmployees
    With ocwEmployee
        If .Salary = "Peanuts" Then
            ocfEmployees.Remove .Index    ' Save the peasants.
        Else
            ocfEmployees.Delete .Index    ' Punish the guilty.
        End If
    End With
Next  'ocwEmployee
ocfEmployees.Persist     ' Exact revenge.

One important point to note here is that worker objects cannot commit suicide! Only the factory object has the power to delete or remove a worker object.

Public Sub Remove(i_vKey As Variant)
    colPicwWorkers.Remove i_vKey
End Sub

Public Sub Delete(i_vKey as Variant)
    If VarType(i_vKey) = vbString Then
        oPicRecordset.AbsolutePosition = _
            colPicwWorkers.Item(i_vKey).AbsolutePosition
        oPicRecordset.Delete
        colPicwWorkers.Remove oPicRecordset.AbsolutePosition
    Else
        oPicRecordset.AbsolutePosition = i_vKey
        oPicRecordset.Delete
        colPicwWorkers.Remove i_vKey
    End If
End Sub

Getting at the Recordset

As discussed earlier, sometimes it is more efficient to deal with the internal factory recordset directly rather than with the factory object. This is primarily true when dealing with distributed clients that do not have direct access to the worker objects themselves. In this case, the factory object exports this recordset through the Recordset property.

The Recordset property can also be used to regenerate a business object. Imagine a distributed client that has accessed an EmployeeDetails method on an action object and has received the corresponding recordset. The distributed client then shuts the action object down (because, as we will soon see, action objects are designed to be stateless). This recordset is then modified and sent back to the action object. The action object needs to perform some operations on the business objects that are currently represented by the recordset.

The action object can create an empty factory object and assign the recordset sent back from the client to this factory. Calling the Populate method will now result in a set of worker objects being regenerated from this recordset! Or, if the data has just been sent back to the database, the action object could call Persist without performing the Populate method at all, again maximizing performance when the client is modifying simple sets of data.

Take particular care when using the Recordset property with distributed clients, though. It's important to ensure that other clients don't modify the underlying business object after the business object has been serialized as a recordset. In such a case, you'll end up with two different recordsets—the recordset on the client and the recordset inside the business object. This situation can easily be avoided by ensuring that the action objects remain as stateless as possible. In practice, this means closing down the business object immediately after the recordset has been retrieved, thereby minimizing the chance of the business object changing while a copy of the recordset exists on the client.

Dim ocfEmployees     As New cfEmployees
Dim ocParams         As New cParams
Dim ocRecordset      As cRecordset

Set ocfEmployees = New cfEmployees

ocParams.Add "PostCode", "GL543HG"

ocfEmployees.Create oPicDAL
ocfEmployees.Populate ocParams

Set ocRecordset = ocfEmployees.Recordset

Be aware that the above code is not recommended, except when you need to return sets of data to distributed clients. Data manipulation that is performed on the same tier as the factory objects should always be done by direct manipulation of the worker objects.

Determining the Parameters of a Factory

Because we don't use explicit procedure parameters in the Populate method of the factory class, it can be useful to be able to determine what these parameters are. The read-only Parameters property returns a cParams object populated with the valid parameter names for this factory.

The Parameters property is useful when designing tools that interact with business objects—such as the business object browser that we'll look at later on—since the parameters for a factory can be determined at run time. This determination allows us to automatically instantiate factory objects.

Dim ocfEmployees        As New cfEmployees
Dim ocParams            As New cParams

Set ocfEmployees = New cfEmployees
Set ocParams = ocfEmployee.Parameters

' Do stuff with ocParams.

Worker Objects

So far, we've concentrated mainly on the factory objects; now it's time to examine in greater detail the construction of the worker objects.

Factory objects all have the same interface. Worker objects all have unique interfaces. The interface of our sample Employee worker object is shown below.

cWorker Employee Interface
Member | Description
ID | Unique string identifier for the worker object
Name | Employee name
Salary | Employee gross salary
Department | Department the employee works in
Create | Creates a new worker object

Creating a worker

As we saw in the discussion of the factory object, creating a worker object involves passing the worker object a reference to the shared recordset and a reference to the factory's DAL object. This is what the worker does with these parameters:

Private oPicRecordset      As cRecordSet
Private lPiRowIndex        As Long
Private oPicDAL            As cDAL

Friend Sub Create(i_ocRecordset As cRecordSet, i_ocDAL As cDAL)
    Set oPicRecordset = i_ocRecordset
    Set oPicDAL = i_ocDAL
    lPiRowIndex = I_ocRecordset.AbsolutePosition
End Sub

Why is this procedure so friendly? (That is, why is it declared as Friend and not as Public?) Well, remember that these worker objects are "Public Not Creatable" because we want them instantiated only by the factory object. Because the factory and workers always live in the same component, the Friend designation gives the factory object exclusive access to the Create method. Notice also that the worker objects store the row reference in the module-level variable lPiRowIndex.

Identification, please

In this design pattern, an ID is required for all worker objects. This ID, or string, is used to index the worker object into the factory collection. This ID could be manually determined by each individual factory, but I like having the ID as a property on the object—it makes automating identification of individual worker objects inside each factory a lot easier.

In most cases, the ID is the corresponding database ID, but what about when a worker object is created based on a table with a multiple field primary key? In this case, the ID would return a concatenated string of these fields, even though they would exist as explicit properties in their own right.

Show me the data!

Here is the internal worker code for the ID property—the property responsible for setting and returning the worker object ID. Note that this code is identical for every other Property Let/Get pair in the worker object.

Public Property Get ID() As Long
    PiSetAbsoluteRowPosition
    ID = oPicRecordset("ID")
End Property

Public Property Let ID(i_ID As Long)
    PiSetAbsoluteRowPosition
    PiSetPropertyValue "ID", i_ID
End Property

The most important point here is the PiSetAbsoluteRowPosition call. This call is required to point the worker object to the correct row in the shared recordset. The recordset current record at this point is undefined—it could be anywhere. The call to PiSetAbsoluteRowPosition is required to make sure that the worker object is retrieving the correct row from the recordset.

Private Sub PiSetAbsoluteRowPosition()
    oPiRecordset.AbsolutePosition = lPiRowIndex
End Sub

Likewise, this call to PiSetAbsoluteRowPosition needs to happen in the Property Let. The PiSetPropertyValue procedure merely edits the appropriate row in the recordset.

Private Sub PiSetPropertyValue(i_sFieldName As String, _
                               i_vFieldValue As Variant)
    oPiRecordset.Edit
    oPiRecordset(i_sFieldName) = i_vFieldValue
    oPiRecordset.Update
End Sub

Methods in the madness

At the moment, all we've concentrated on are the properties of worker objects. What about methods?

An Employee worker object might have methods such as AssignNewProject. How do you implement these methods? Well, there are no special requirements here—implement the customer business methods as you see fit. Just remember that the data is in the shared recordset and that you should call PiSetAbsoluteRowPosition before you reference any internal data.

Worker objects creating factories

Factory objects returning workers is all well and good, but what happens when we want to create relationships between our business objects? For example, an Employee object might be related to the Roles object, which is the current assignment this employee has. In this case, the Employee worker object will return a reference to the Roles factory object. The Employee object will be responsible for creating the factory object and will supply any parameters required for its instantiation. This is great because it means we need only to supply parameters to the first factory object we create. Subsequent instantiations are managed by the worker objects themselves.

Dim ocDAL            As New cDAL
Dim ocfEmployees     As New cfEmployees
Dim ocwEmployee      As New cwEmployee
Dim ocParams         As New cParams

ocfEmployees.Create ocDAL
ocParams.Add "ID", "637"
ocfEmployees.Populate ocParams

MsgBox ocfEmployees(1).Roles.Count

Here the Roles property on the Employee worker object returns the ocfRoles factory object.

Public Property Get Roles() As cfRoles
    Dim ocfRoles As New cfRoles
    Dim ocParams As New cParams

    ocParams.Add "EmpID", Me.ID
    ocfRoles.Create oPicDAL
    ocfRoles.Populate ocParams
    Set Roles = ocfRoles
End Property

Accessing child factory objects this way is termed navigated instantiation, and you should bear in mind this important performance consideration. If I wanted to loop through each Employee and display the individual Roles for each Employee, one data access would retrieve all the employees via the DAL and another data access would retrieve each set of Roles per employee. If I had 1800 employees, there would be 1801 data access operations—one operation for the Employees and 1800 operations to obtain the Roles for each employee. This performance would be suboptimal.

In this case, it would be better to perform direct instantiation, which means you'd create the Roles for all employees in one call and then manually match the Roles to the appropriate employee. The Roles object would return the EmployeeID, which we would then use to key into the Employee factory object to obtain information about the Employee for this particular Roles object. The golden rule here is that navigated instantiation works well when the number of data access operations will be minimal; if you need performance, direct instantiation is the preferred method.

Dim ocfRoles         As New cfRoles
Dim ocfEmployees     As New cfEmployees
Dim ocwRole          As cwRole

ocfEmployees.Create oPicDAL
ocfRoles.Create oPicDAL

ocfEmployees.Populate     ' Using default populate to retrieve all objects
ocfRoles.Populate

For Each ocwRole In ocfRoles
    MsgBox ocfEmployees(ocwRole.EmpID).Name
Next ' ocwRole

An interesting scenario occurs when a worker object has two different properties that return the same type of factory object. For example, a worker could have a CurrentRoles property and a PreviousRoles property. The difference is that these properties supply different parameters to the underlying factory object Populate procedure.

Where are my children?

It's useful to be able to query a worker object to determine what factory objects it supports as children. Therefore, a worker object contains the read-only property Factories, which enables the code that dynamically determines the child factory objects of a worker and can automatically instantiate them. This is useful for utilities that manipulate business objects.

The Factories property returns a cParams object containing the names of the properties that return factories and the names of those factory objects that they return. Visual Basic can then use the CallByName function to directly instantiate the factories of children objects, if required.

The Factories property is hidden on the interface of worker objects because it does not form part of the business interface; rather, it's normally used by utility programs to aid in dynamically navigating the object model.

Dim ocfEmployees     As New cfEmployees
Dim ocfEmployee      As New cfEmployee
Dim ocParams         As New cParams

ocfEmployees.Create oPicDAL
ocfEmployees.Populate
Set ocfEmployee = ocfEmployees(1)

Set ocParams = ocfEmployee.Factories

Business Object Browser

After you've created the hierarchy of business objects by having worker objects return factory objects, you can dynamically interrogate this object model and represent it visually. A business object browser is a tremendously powerful tool for programmers to view both the structure and content of the business objects, because it allows the user to drag and drop business objects in the same fashion as the Microsoft Access Relationships editor.

Business object wizard

Creating business objects can be a tedious task. If the data source you're modeling has 200 major entities (easily done for even a medium-size departmental database), that's a lot of business objects you'll have to build. Considering that the factory interface is the same for each business object and that the majority of the properties are derived directly from data fields, much of this process can be automated.

A business object wizard works by analyzing a data entity and then constructing an appropriate factory and worker class. This is not a completely automated process, however! Some code, such as worker objects returning factories, must still be coded manually. Also, any business methods on the worker object obviously have to be coded by hand, but using a business object wizard will save you a lot of time.

TMS uses a business object wizard to develop factory-worker classes based on a SQL Server database. This wizard is written as an add-in for Visual Basic and increases productivity tremendously by creating business objects based on the Shared Recordset implementation. If you need simple business objects, though, you can use the Data Object wizard in Visual Basic 6.0. The mapping of relational database fields to object properties is often referred to as the business object impedance mismatch.

Business object model design guidelines

Keep it simple—avoid circular relationships like the plague. Sometimes this is unavoidable, so make sure you keep a close eye on destroying references, and anticipate that you might have to use explicit destructors on the factory objects to achieve proper teardown. Teardown is the process of manually ensuring that business objects are set to Nothing rather than relying on automatic class dereferencing in Visual Basic. In practice, this means you call an explicit Destroy method to force the release of any internal object references.

Don't just blindly re-create the database structure as an object model; "denormalization" is OK in objects. For example, if you had an Employee table with many associated lookup tables for Department Name, Project Title, and so forth, you should denormalize these into the Employee object; otherwise, you'll be forever looking up Department factory objects to retrieve the Department Name for the employee or the current Project Title. The DAL should be responsible for resolving these foreign keys into the actual values and then transposing them again when update is required. Typically, this is done by a stored procedure in a SQL relational database.

This doesn't mean you won't need a Department factory class, which is still required for retrieving and maintaining the Department data. I'm saying that instead of your Employee class returning the DepartmentID, you should denormalize it so that it returns the Department Name.

MsgBox ocfEmployee.DepartmentName

is better than

MsgBox ocfDepartments(ocfEmployee.DepartmentID).Name

But at the end of the day, the level of denormalization required is up to you to decide. There are no hard and fast rules about denormalization—just try to achieve a manageable, usable number of business objects.

Wrap Up

So there's the basic architecture for factory-worker model business objects with a Shared Recordset implementation. I feel that this approach provides a good balance between object purity and performance. The objects also return enough information about themselves to enable some great utilities to be written that assist in the development environment when creating and using business objects.


Action Objects

Now we turn to where the rubber meets the road—the point where the client applications interface with the business objects, which is through action objects.

Action objects represent the sequence of actions that an application performs. Action objects are responsible for manipulating business objects and are the containers for procedural business logic. Remember that the distributed clients should only send and receive cRecordset or cParams objects. This is a thin-client approach. The client should be displaying data, letting the user interact with the data, and then it should be returning this data to the action object.

Action objects live in the logical business tier (with factory and worker objects). Therefore, access to these action objects from the clients should be as stateless as possible. This means that if action objects live on a separate physical tier (as in a true three-tier system), performance is maximized by minimizing cross-machine references.

The other important task of action objects is to define transaction scope; that is, when an action object is created, all subsequent operations on that action object will be in a transaction. Physically, action objects live in a Visual Basic out-of-process component or in DLLs hosted by MTS. Worker and factory objects are contained in an in-process component that also could be hosted in MTS.

The structure of action objects comes from the requirements of the application. The requirements of the application logic should be determined from an object-based analysis, preferably a UML usage scenario. This is what I refer to as the Golden Triangle.

Here's an example: Imagine the archetypal Human Resource system in any organization. One of the most basic business requirements for an HR system is that it must be able to retrieve and update employee information and the employee's associated activities. These activities are a combination of the employee's current roles and projects and are known as the usage scenario.

The user interface needed to meet this requirement could be a Visual Basic form, but it could also be an Active Server Page-based Web page.

Action Object Interface

We can imagine that a Visual Basic form that implements this usage scenario would present two major pieces of information: EmployeeDetails and Activities. We can now determine the interface of the required action object.

  • GetEmployeeDetails

  • UpdateEmployeeDetails

  • GetCurrentActivities

  • UpdateCurrentActivities

Notice that these attributes of the action object are all stateless. That is, when you retrieve a recordset from GetEmployeeDetails, you then shut down the action object immediately, thereby minimizing the cross-layer communication cost. Users can then modify the resulting recordset and, when they are ready to send it back for updating, you create the action object again and call the UpdateEmployeeDetails method. The action object does not need to be held open while the recordset is being modified. Let's look at these calls in more detail:

Public Function GetEmployeeDetails(Optional i_sID As String) As cRecordset

    Dim ocfEmployees      As New cfEmployees
    Dim oParams           As New cParams

    If Not IsMissing(i_sID) Then oParams.Add "ID", i_sID

    ocfEmployees.Create oPicDAL
    ocfEmployees.Populate oParams

    Set GetEmployeeDetails = ocfEmployees.Recordset

End Function

Likewise, the UpdateEmployeeDetail call looks like this:

Public Sub UpdateEmployeeDetail(i_ocRecordset As cRecordset)

    Dim ocfEmployees As New cfEmployees
    Dim ocRecordset  As New cRecordset

    Set ocRecordset = New cRecordset
    ocRecordset.Serialize = i_ocRecordset.Serialize ' Create local
                         ' copy of object so that business objects do
                         ' not have to refer across network.
    ocfEmployees.Create oPicDAL
    Set ocfEmployees.Recordset = ocRecordset
    ocfEmployees.Persist

    i_ocRecordset.Serialize = ocRecordset.Serialize ' Copy updated
                         ' recordset back to client.

End Sub

The GetCurrentActivities call has a bit more work to do. It must create a new recordset because the usage scenario requires that both Roles and Projects come back as one set of data. So the GetCurrentActivities call would create a recordset with three fields—ActivityID, ActivityValue, and ActivityType (Roles or Projects) and then populate this recordset from the Roles business object and the Projects business object. This recordset would then be returned to the client.

The UpdateCurrentActivities call would have to do the reverse—unpack the recordset and then apply updates to the Roles and Projects table.

Transactions in Action

So if action objects are responsible for transactions, how do they maintain transactions? When an action object is initiated, it instantiates a cDAL object and begins a transaction. This cDAL object is passed to all business objects that the action object creates so that every business object in this action object has the same transaction.

Just before the action object is destroyed, it checks a module-level variable (bPiTransactionOK) in the class Terminate event to see whether the transaction should be committed. This module-level variable can be set by any of the procedures within the action object. Normally, if a transaction has to be rolled back, an exception is raised to the client and bPiTransactionOK is set to False so that the user can be informed that something very bad has happened. Checking this module-level variable in the Terminate event ensures that the action object is responsible for protecting the transaction, not the client.

Private bPiTransactionOK    As Boolean
Private oPicDAL             As cDAL

Private Sub Class_Initialize()
    Set oPicDAL = New cDAL
    oPicDAL.BeginTransaction
    bPiTransactionOK = True
End Sub

Private Sub Class_Terminate()
    If bPiTransactionOK Then
        oPicDAL.CommitTransaction
    Else
        oPicDAL.RollbackTransaction
    End If
End Sub

Wrap Up

So now we've covered the role and design of action objects. In summary, a good analogy is that of a relational SQL database: factory-worker objects are represented by the tables and records, and action objects are represented by the stored procedures. The lifetime of the action object controls the lifetime of the implicit transaction contained within.

Action objects should be accessed in a stateless fashion—get what you want and then get the hell out of there! Stateless fashion is enabled by the supporting of serialization by cRecordset and cParams, which ensures that your applications can achieve good distributed performance.


Clients

Now for the final part of the puzzle. What is the best way for our client applications to use action objects? Basically, the client should open the appropriate action object, retrieve the required recordset, and then close down that action object immediately. When the data is ready to be returned for update, the action object will be instantiated again, the relevant Update method will be called, and then the action object will be closed down again. Here is an example of a client calling an action object to retrieve the list of current employees:

Dim ocaEmployeeMaintenance   As caEmployeeMaintenance
Dim ocRecordset              As cRecordset

Set ocRecordset = New cRecordset
Set ocaEmployeeMaintenance = _
    New caEmployeeMaintenance ' Open Employee action object

ocRecordset.Serialize = _
    caEmployeeMaintenance.GetCurrentEmployees.Serialize
Set ocaEmployeeMaintenance = Nothing

' Do stuff with local ocRecordset in user interface
' ...

Set ocaEmployeeMaintenance = New caEmployeeMaintenance
ocaEmployeeMaintenace.UpdateEmployees ocRecordset
Set ocaEmployeeMaintenance = Nothing

This code could be in a Visual Basic form, in an Active Server Page, or even in an ActiveX document—the important thing is that the client reference to the action object is as quick as possible. The recordset maintains the data locally until the time comes to the send the data back to the action object.

Wrap Up

So there you have it, a workable approach to implementing high-performance distributed objects. With the right amount of planning and design, an awareness of distributed application issues, and the power of Visual Basic, building powerful, scalable software solutions is well within your reach.

So get out there and code those business objects!


Chapter 3 -- IIS This A Template I See Before Me?

Chapter 3

IIS This a Template I See Before Me?

Developing Web Applications

ROGER SEWELL

Roger's origins in programming lie in a small room in a Leicestershire school, where he spent many a happy hour punching out tape on a teletype terminal before connecting to a mainframe via an acoustic coupler modem and finding that his program didn't work. Between then and the start of his Visual basic programming career, he spent his time in the murky world of mainframes bashing out Fortran programs for scientists. Roger first saw the VB light in early 1994 and hasn't looked back since. Roger lives in Oxfordshire with his wife Kath and their young family. When not playing or watching sports Roger fights a losing battle against the jungle growing around the house.

When I first fired up Microsoft Visual Basic 5 (yes, I did say Visual Basic 5), I was surprised to be presented with a box asking me what type of project I wanted to develop. I was even more surprised to find that there were nine project templates to choose from—who would have thought that little old Visual Basic would grow into such a multitalented individual? Since that day I have become accustomed to the variety of development options given to us by Visual Basic, and now Visual Basic 6 offers us three new templates: Data Project, IIS Application, and DHTML Application.

I'll examine the IIS Application in this chapter, going through the process of developing a simple application for Internet Information Server (just in case you hadn't worked out the acronym).

This template and the new features embodied in it are intended to give Visual Basic developers the opportunity to develop Web applications without leaving (too often) the environment they know so well. I hope I'll be able to give you a feel for the development process, because it is different from the process of a normal Visual Basic project. I also want to get you thinking differently about how your application should be structured and how your users will interact with your application, because these design issues are probably the biggest change that you as a developer of a Web application (many such applications, I hope) are going to face.


Just What Is a Web Application?

Before I go any further, I ought to explain just what I mean by a "Web application." A Web application is an application that runs on a Web server and is accessed at the client end using an Internet browser program or other program with Web browsing facilities. In this model, all the processing in the application is performed on the Web server and the client receives HTML-coded pages that contain the user interface and client data. Of course, real-world applications contain a mixture of server-side and client-side processing. Browsers can download Microsoft ActiveX components and Java applets to add richer functionality to the interface of the application. Web pages can contain scripting code to run at the client end to provide further flexibility in the front end and preprocessing of data and requests for the back end.

The Web application model offers the prospect of a single client front end that is capable of accessing a variety of different applications on a remote server. While this front end might not be the thinnest client in the world, it can be a fairly universal client. This client could also be updatable remotely. If it doesn't have a particular capability required by an application (such as the display of a particular format of data), the client could download a component that it can use to give it that capability. Similarly, the versions of components the client already has could be checked to ensure that they are always up to date.

A Web application in the corporate intranet obviously is attractive for a variety of reasons. By having a single client application for all server applications, rollouts are simplified and client PC maintenance costs are reduced. Standard machine setups can be used with the client gradually updating itself as it accesses different applications. In the corporate environment, where there can be tight control over the software on a user machine, applications can be produced for a specific browser, thus allowing developers to fully exploit the capabilities of that browser. Web applications also allow people to work from home or away from their normal office location without losing functionality.

Developing applications for the Internet means you have a huge audience that can access your work. Unlike the corporate intranet environment, you can no longer assume that all browsers accessing your application will have the same capabilities, but this is by no means a barrier to deploying applications over the Internet.

IIS or ASP?

Before the IIS Application template was introduced to Visual Basic, developers wanting to produce Web applications had Active Server Pages (ASP) technology available to them. The ASP model allowed developers to write pages that could be accessed from a browser and that performed some processing on the Web server before returning HTML to the browser. The server-side code could be written in a variety of languages (including client-side scripting languages, such as VBScript or Microsoft JScript), so long as the server had access to a scripting engine for that language. ASP provides a number of different objects for server scripts that allow interaction with both the client and the server.

IIS applications, while based on ASP, provide a better way for Visual Basic developers to produce Web-based applications. For example, the development environment is familiar (though there are differences for IIS application development, as you'll see). The language is the same, so you don't have to learn a new scripting language. The processing code and visual interface code can be separated out to provide a cleaner project—ASP pages have script and interface code intermingled. Visual Basic is also a much more powerful language in which to develop applications than is a scripting language (even if the scripting language can call on code components to perform any tricky processing).

At the time of this writing (on the March beta of Visual Studio 6), there is some MSDN documentation on developing IIS applications but no example code (apart from the fragments appearing in the documentation). So, while I'm not flying completely blind on this one, let's say there're some low clouds and all I have are the instruments to guide me (plus a sick bag and spare underwear!).


What Does Visual Basic 6 Need for Web Application Development?

OK, so what do you need to start developing an IIS application? Even if you don't run your application you'll still need the Microsoft Active Server Pages Object Library, ASP.DLL. This is included in the project references in the IIS Application template, and one of its objects (the Response object) is used in the template code. However, a Web server that supports ASP is required to run your application. The Microsoft choices for development Web servers are shown in Table 3-1.

Table 3-1 Web Application Development Servers
Operating System | Development Web Server
Windows NT Server 4 | Internet Information Server
Windows NT Workstation 4 | Peer Web Services
Windows 9x | Personal Web Server

You can use any of these servers during application development, but you must use IIS for deployment.

Now that we have discussed IIS applications in general, let's try developing one of our own. First we'll see what the IIS Application template gives us for free. Then we'll develop a home page for our application and add further extensions to our home page to increase its usefulness to users and to ourselves.

Foundations: The Free Stuff

Assuming you've installed and set up your Web server, the next step is to fire up Visual Basic 6. Select New Project from the File menu to display the New Project dialog box, and then select the IIS Application template to start developing your new application.

Right away you'll notice something different—the Project Explorer window contains a folder called Designers in the project tree. Designers, which act as hosts for a particular type of object, appeared in Visual Basic 5, although none of the templates supplied with Visual Basic 5 used them. Designers could (and still can) be added to a project by selecting Components from the Project menu, choosing the Designers tab of the Components dialog box, and then checking a designer in the list. Designers have features that enable the development of particular objects within the Visual Basic integrated development environment (IDE). In Visual Basic 5, two designers came as standard components; now there are seven. IIS Applications use the WebClass designer to develop WebClass objects. Each IIS application contains at least one WebClass object.

The IIS Application template provides you with a single item in your new project; a WebClass object named WebClass1. Even at this early stage your project is in a state where it can be run (and can demonstrate the fact that it is running). Clicking the Start button on the toolbar (or using any of the other methods to start a project in the IDE) brings up a scaled-down version of the Project Properties dialog box with only the Debugging tab visible. This allows you to specify which WebClass component you want to start the application with in an IIS application. This dialog box also contains a Use Existing Browser check box, which, when selected, will launch the application in a browser that's currently running. If this option is not checked, Visual Basic will launch Internet Explorer 4 to act as the client for your application. Even if Internet Explorer 4 is not your default browser, Visual Basic will still launch Internet Explorer 4 (and then ask you if you want to make it your default browser). The options available when you start the project in the IDE are shown in Table 3-2.

Table 3-2 Project Debugging Options

Startup Option | Possible Use
Wait For Components To Be Created | Simulates a more normal method of operation, where the application is already running on a Web server and waits for browser requests
Start Component | Opens browser and automatically accesses the specified component
Start Program | Accesses the application using a different browser or a program with browsing capabilities
Start Browser With URL | Accesses a particular part of your application or a page with a link to your application

Once you have chosen your preferred options from this dialog box, you will be prompted to save your project and its files. In an IIS application the directory where you save your project has a bit more significance than normal since this is where the temporary files created at run/debug time will be placed. It is also the same directory where the HTML template files used in the application will be stored. When your application is deployed on a Web server, the directory structure used for development will be mirrored in the production directories, the only change being to the root directory for the application. As far as IIS is concerned, all directories stemming from a virtual root directory are part of the same Web application, unless they themselves are the virtual root directory for another application.

Once you've saved your project files, you'll be prompted for the name of a virtual directory that the Web server should use to host this application during development. When you deploy your application on your production Web server, you'll be able to specify into which directory you want to place your application files. You'll also be able to specify which virtual directory you want your Web server to associate with the physical application directory. The virtual directory name will form part of the URL that browsers use to reference your application.

Finally, after selecting a name for the virtual directory, your application will start. To see your application running, you'll have to switch to a Web browser. If you've chosen to start with a particular Web class, the focus will automatically have been transferred to a browser. If you have not yet entered any code, you'll be greeted by a Web page with the heading "WebClass1's Starting Page."

It's worth taking a moment to have a look in the project directory for the application. In addition to seeing the Web class designer files and project files, you'll also see that an ASP file has been created. During development, this is a temporary file that gets created whenever you start a debug session for your application and is destroyed when you finish the debug session. If you examine this file in Notepad, you'll see that this ASP file (WEBCLASS1.ASP for the default project) contains the following code:

<%
Server.ScriptTimeout=600
Response.Buffer=True
Response.Expires=0

If (VarType(Application("~WC~WebClassManager")) = 0) Then
    Application.Lock
    If (VarType(Application("~WC~WebClassManager")) = 0) Then
        Set Application("~WC~WebClassManager") = _
            Server.CreateObject("WebClassRuntime.WebClassManager")
    End If
    Application.UnLock
End If

Application("~WC~WebClassManager").ProcessNoStateWebClass _
    "Project1.WebClass1", _
    Server, _
    Application, _
    Session, _
    Request, _
    Response
%>

This is an ASP script (as shown by the script delimiters <% and %>) that uses the ASP Server and Response objects and creates an instance of our Web class. From this script, we can see that our IIS application is a single-page ASP application that simply runs an instance of a single object. Normally, as already mentioned, an ASP application would be made up of ASP files containing a mixture of script code to be processed by the Web server and HTML to be returned to the browser.

If we now shut down the browser and return to Visual Basic, we see that the application is still running. The temporary ASP used to access our application is still in existence, so we can start the browser and run the application again. We can even connect to our application from another computer if the Web server has been set up to allow access from that computer. You must close down your application from within Visual Basic, which causes the temporary ASP file to be deleted and thus denies access to your application.

So that's the Web application equivalent of "Hello World" done without writing even a single line of code! (As far as "Hello World" applications go, a Web application is likely to greet more of the world than a C application or a Visual Basic application.)

Building a Home of Your Own

If you want your IIS application to do anything useful, now is the time to get familiar with HTML. There is no getting away from HTML in a Web application—without it you have no user interface to your application. You can distance yourself from HTML to a certain extent by having someone else design your Web pages/client interface for you, but this will probably feel alien to the majority of Visual Basic developers who are used to dragging controls from the toolbox and designing their own forms.

Using HTML to design a user interface harkens back to the days when visual elements were created by writing code that had to be executed before the developer could see what they actually looked like on screen. However, plenty of applications allow you to design your Web pages in a visual way and then generate the underlying HTML for the Web page for you automatically. An understanding of the HTML behind the page is at least useful (if not essential) for effective Web application development, because it is the HTML in your user interface that furnishes your application with events that it can respond to.

You can include HTML in your application in two ways. The most flexible technique (and the most laborious) is to include raw HTML code in your procedures. You will already have seen this approach if you've examined the code supplied by the IIS Application template. Here is an example that uses the Write method of the ASP Response object to send text to the browser:

Response.Write "<HTML><BODY>Hello World!!</BODY></HTML>"

The other way of including HTML in your application is to add HTML files to your Web class through an HTML template. When an HTML template is added to a Web class, Visual Basic takes a copy of the underlying HTML file and stores the copy in the project directory. The HTML template is referenced in the Web class by the Web item name that you assign to it when it is added to the Web class.

When adding HTML templates to a Web class, the normal default naming convention of Visual Basic is followed, meaning that the item type name is suffixed with a number that makes the name unique. While this is fine in most cases, it would seem more sensible, when adding HTML templates, that the name of the original HTML file be used to refer to the template. The copies of the files held in the project directory maintain their original names (with the addition of a numeric suffix if a file of the same name already exists in the project directory), so why not default to these names in the IDE? So, when you add an HTML template to a Web class, change the default name to that of the HTML file it represents (remember to check the project directory to see if the name has been changed).

The HTML file is now in the Web class, but no one will get to see it unless the Web class is told when to display it. An HTML file is sent to the browser when the WriteTemplate method of the HTML template is called within code. This can be done from the Start event of the Web class but is better placed in the Respond event of the HTML template Web item. The Respond event is the default event of any template and is called whenever the template is referenced without a specific event procedure. To reference the HTML template in code, the NextItem property of the Web class is set. At the end of processing an event in a Web class, if the NextItem property points to a Web item, processing is transferred to that Web item. This is the statement to place in the Start event of the Web class in order to have the template displayed in the browser:

Private Sub WebClass1_Start()
    Set NextItem = Welcome
End Sub

Private Sub Welcome_Respond()
    Welcome.WriteTemplate
End Sub

In the above code, when the Web class starts, the HTML template named Welcome is sent to the browser requesting the Web class's ASP.

An HTML template is able to trigger events within a Web class when the template is processed by a browser. When an HTML template is added to a Web class, its underlying HTML file is scanned for tags that have attributes that can take a URL as a value. If an attribute can have a URL for a value, it can send HTTP requests to the Web server, which can be intercepted by the Web class. The Web class can intercept these requests only if the tag attributes are connected to events in the Web items of the Web class. The connection of attributes to events is performed in the right-hand pane of the Web class designer. Attributes can be connected either to a custom event or to a Web item by right-clicking a tag in the designer and choosing one of the options presented. If the attribute is connected to a custom event, the designer automatically creates a new event for the template containing the tag. Default event names come from the tag they are connected to (combined with the name of the attribute if the tag has multiple attributes that can be connected to events, as shown in Figure 3-1).

Figure 3-1 Web class designer showing custom events

If you change an event name (using the same techniques as in Windows Explorer), it is automatically updated both in the Target column of the Web class designer and the HTML Context column, but only for that tag-attribute combination. The update is not shown in other HTML Context entries belonging to the same tag until the template is refreshed in the designer. Also, if any code has been written for a custom event and the event's name is changed, the code becomes disassociated from the event and exists as a general procedure in the Web class (as happens when renaming a control in a form-based Visual Basic application). The names used for tags in the designer come from the tags themselves unless an ID or Name attribute has been set for the tag (with the ID attribute taking precedence).

Extending Your Home to Guests

Now we have a page to display when a user connects to our application. Nothing very exciting, and just maybe our visitors will want to tell us this. Being well-adjusted individuals who aren't afraid of what people might say about our work, we'll include a guestbook in our application. (See Figure 3-2.) We'll store the comments in a database and display the five most recent ones whenever anyone accesses our guestbook. We need a place for visitors to enter their comments, a mechanism for them to register their comments, and a space to display previous comments. To be helpful, we'll provide a way of clearing the comment box.

Figure 3-2 Guestbook Web page

Although the guestbook appears to be a single Web page, it has been created from two separate Web items. Everything above the horizontal rule comes from an HTML template called Guestbook that we have added to our Web class. The horizontal rule and the table below it come from a custom Web item called RecentComments. A hypertext link has been placed on our Welcome page. A connection has been made between the HREF attribute of the hyperlink and a custom event of the Welcome template, which we have called DisplayGuestbook. This custom event simply sets the NextItem property of the Web class to the Guestbook template and relies on the processing in the Respond event of the template to provide output to the browser. The Respond event for the Guestbook template sets the NextItem property to the RecentComments custom Web item before calling its own WriteTemplate method. At the end of the Guestbook template's Respond event, processing passes to the RecentComments custom Web item's Respond event. In this second Respond event a private procedure, GetComments—which is defined in the Web class, not in a Web item—is called to dynamically generate an HTML table definition containing the previous five comments. This table and a horizontal rule used to split the screen are written directly to the browser using the Response object of the Web server. The GetComments procedure uses ActiveX Data Objects (ADO) code to query our guestbook database. The query returns the comments to display in the Five Most Recent Comments table.

When the user has entered a comment, he or she clicks the button labeled Submit Comments to submit it. When this button is clicked all the responses generated by the form are gathered together and sent back to the server. Although individual input elements, like buttons or the comment text box, are not listed in the Web class designer, the form that contains them is listed in the designer. By connecting a custom event to the Form tag of the Guestbook template, we can program a response to the visitor submitting comments. Input controls in HTML are contained within sections called forms. The three attributes of forms that primarily concern us as IIS application developers in Visual Basic are: the ID attribute, which sets the name under which the form will be listed in the Web designer; the Action attribute, which we will be connecting to in order to trigger our response; and the Method attribute, which sets the mechanism by which data is sent from the browser to the server. For IIS applications written using Visual Basic, POST is the only value for the Method attribute that we can use. If we try to include an HTML template containing a Form tag with the Method attribute set to GET (or even without the Method attribute at all), the Web class designer issues a warning message (as shown in Figure 3-3) and tells us what it is going to change in our template.

Figure 3-3 Warning message issued by the Web class designer

When the POST method is used, data is received by the server in the Form collection property of the Request object. Each element of the Form that generates data creates a member in the Form collection using the element's Name attribute as its key in the collection. The data returned by each element is held in the Item property of each collection member. Since this is the default property of the collection, we can determine if the SubmitComments button was clicked using the following piece of code:

If LCase$(Request.Form("submitcomments")) = "submit comments" Then

The value returned by this type of button (a Submit button) is always the same as the text displayed on the button. If the button had not been clicked, there would be no member of the Form collection with the key "submitcomments." Having received the visitor's comment about our site (in the member of the Form collection with the key "comments"), we then store the comment in our database. Another procedure called AddComment takes care of this; again, this is a private method of the Web class. The visitor remains on the Guestbook page, which has the list of recent comments updated to reflect the visitor's newly added comment.

Turning Your Home into a Business

We have a starting page for our Web application and we have a guestbook that users of our application can write comments into, but we don't as yet really have an application. How about we use our application to provide a series of technical articles, say, a whitepaper distribution application? Now, we at TMS have invested considerable resources into creating these whitepapers, so we don't want to give them away to all and sundry with no return whatsoever. So we'll provide summaries of all our whitepapers that anyone can access but will require users to provide us with some information before they can access the full text of any of the whitepapers.

To accomplish this, the first thing we'll have to do is alter our Welcome page. At a minimum, we need to describe the application so that users know what they can expect. We also want to differentiate between anonymous users who don't want to register and those users who do. Repeat users will probably want to sign in on the Welcome page and have full access to the application from then on. New users will probably want to view some of the whitepaper summaries before parting with personal information. This means that we'll have to have the facility to dynamically disable some of the features of our Web pages. We'll also want to be able to access common Web pages (such as the Registration page) from multiple locations within the application. Having obtained personal information from users, we might feel inclined to make them more at home by customizing the pages presented to them, too.

We've redesigned our Welcome page to be more descriptive of the application, added some graphical navigation buttons, and increased the number of options that a user has on this page. (See Figure 3-4.)

Figure 3-4 New Welcome page

Graphical navigation buttons were used partly for their visual appeal (even done as simply as this!) but they were used mostly because each graphical button can trigger an event individually. This is because the buttons actually are graphical hypertext links whose HREF attributes can be connected to custom events (or Web items) if we choose. (They are not Input tag controls wrapped inside Form tags and therefore don't have to be accessed through the Request object's Form collection.) The HREF attribute of each of the buttons references the corresponding HTML template (SignIn, Guestbook, and so forth). The Respond event in each of these templates simply calls the WriteTemplate method of the template to display the HTML page in the browser. So the only new feature in the Welcome template is the use of graphical buttons instead of straight text links or "proper" input buttons inside an HTML form. The guestbook itself remains the same, but there is more to comment about in the application, so it serves more of a purpose!

The Registration page is new, but the techniques behind it aren't. A single form contains all the input fields. The data entered on the page is returned in the Request.Form collection. To handle the registration of users in our WEBAPP.MDB database, we have added a class module to provide us with a CUser object. This class has properties that match the information we want to capture about our users. The CUser class also has a method, RegisterUser, which adds the data held in the properties of a CUser object to the database. This method has a return value of True if the user was successfully added to the database (the value is otherwise False). Armed with this information, we can control what happens next. If the RegisterUser method fails, we want to redisplay the registration form to allow the user to re-enter their details. If the user was successfully added to the database, several things should happen. First we ought to move the user to a different area of the application, away from the registration form, maybe by sending them back to the page where they jumped from to get to the registration page. Alternatively, we could send them to a fixed page of our choosing, perhaps with a "Thank you for registering" message. To make our application more welcoming, we also want to personalize the pages that we send back to users who have signed in.

Now that we have more pages in our application, we've decided that we want to use the same toolbar on several of those pages. To this end we've created a separate template called Toolbar which duplicates the functionality of the Welcome page toolbar. The next task is to edit the Welcome template to remove the section of HTML connected to the toolbar buttons and allow Visual Basic to refresh the template in the designer.

Finally we need to add code to display the Toolbar Web item at the bottom of the Welcome page. We could use the following code in the Respond event of Welcome:

Welcome.WriteTemplate
Toolbar.WriteTemplate

This does the job of putting the buttons at the bottom of the page, but it is not the code we are going to use. The WriteTemplate method can take the name of another template as its only argument, so it would seem that we could use this code as an alternative:

Welcome.WriteTemplate
Welcome.WriteTemplate Toolbar

If we try this code, however, we find that instead of getting buttons at the bottom of our page we get only another copy of Welcome displayed. (See Figure 3-5.)

Figure 3-5 A doubly warm Welcome!

Toolbar.WriteTemplate Toolbar works as we would expect (perhaps "hope" would be a better word to use), which suggests that the template argument to a WriteTemplate call is ignored. However, we'll use this code:

Welcome.WriteTemplate
Set NextItem = Toolbar

Why use this technique? Because accessing the Toolbar via the NextItem property of the Web class ensures that the Respond event of Toolbar fires, which won't happen if we use the Toolbar's WriteTemplate method. This way, any extra processing we specify in the Respond event is applied before the template is sent to the browser.

Now that the Toolbar Web item has been added to the Welcome page, we'll add it to the Summary page, too. This page is much more interesting, both in terms of HTML construction and also in the Web class and Web item features used. The template used to construct the Summary page is called SummaryLayout. The HTML file this template is derived from uses frames to enable different sources to be used for the content of different areas of the screen within the browser. The following fragment of the original HTML file

<FRAMESET ROWS="25%,50%,25%">
    <FRAME SRC="" NAME="SummaryList">
    <FRAME SRC="" NAME="SummaryView">
    <FRAME SRC="" NAME="SummaryToolbar">
</FRAMESET>

produces the event candidates shown in Figure 3-6 when parsed by the Web class designer.

Figure 3-6 Event candidates for SummaryLayout frames

Now the question is, "Why have we designed the Summary page like this?" After all, if we wanted three different blocks of HTML code outputting to the browser, we could have strung three Web items together to produce the output. We've structured this part of our application like this because of the features we want it to provide to our users (as will become clear later).

Since we already have our Toolbar template written and in place, we'll connect that to SummaryLayout first. We want it to be displayed at the bottom of the page, so we connect the Toolbar Web item directly to the SummaryToolbar tag that is the source for the bottom frame of the page. (See Figure 3-7.)

Figure 3-7 The Connect To WebItem dialog box

Fatal Errors in a Web Class

In a Web application, errors can occur outside of the Visual Basic code. In such cases, traditional error handling techniques (such as those discussed in Chapter 1) are not available for handling the situation gracefully. Instead, you can make use of the FatalErrorResponse event of the Web class. This has a single Boolean argument, SendDefault, which is an output argument. By setting SendDefault to False, the default error message is prevented from being shown in the browser. By using the Response object, you can send your own text to the browser to explain to the user that there has been a problem. The Web class provides an Error object that you can query to determine the Source, Number, and Description relating to the error. Such information can be written to the browser or logged to a file as necessary. On Windows NT 4 systems, the error is also written to the Application Log, where it can be examined in the context of other errors.

There's no problem with the article list in the top frame of the page. We'd always intended this to be generated dynamically in a custom Web item, called WhitepaperList. This Web item generates the list of available whitepapers and formats the list to make best use of the space in the top frame of the page. The names of the whitepapers are presented as hyperlinks so that clicking them sends a request to the server for the whitepaper summary. In HTML, frames can be used as targets for page requests if the frame is given a name. We've done this for the middle frame so that when the browser requests the whitepaper summary from the server, the summary is displayed in the middle frame upon receipt by the browser.

To produce a server request specific to the whitepaper that the user has selected, we use the URLFor method of the Web class and the UserEvent event of the SummaryView custom Web item we've created for displaying whitepaper summaries. The URLFor method generates, at run time, a URL either for an existing Web item event or for a run-time event. The arguments for URLFor are an existing Web item (which is required) and, optionally, the name of an event to fire in the Web item. If no event name is supplied, the Respond event will be fired if a request is made with this URL. If an existing event's name is used, that particular event will be fired in the Web item. The advantage of this method is the ability to specify an event that does not exist at design time—a run-time event:

Response.Write "<a href="""
Response.Write URLFor(SummaryProcessor, "Whitepaper 1")
Response.Write """>Whitepaper 1</a>"

The hyperlink defined above appears as "Whitepaper 1" in the browser and when selected will send a request to the server containing the URL for the "Whitepaper 1" event in the SummaryProcessor Web item. Since this event does not exist at design time, the UserEvent event procedure of the SummaryProcessor Web item is fired. A single argument is passed into the UserEvent event procedure that is the name of the run-time event that was triggered. This technique is well suited to presenting our whitepaper summaries, since it allows us to pass a request for the whitepaper into an event that we can use to send the summary back to the browser.

To generate the Summary page we make use of the ProcessTag event, the only standard event of a Web item that we haven't used yet. This event is fired automatically when the WriteTemplate method of a Web item encounters replacement tags in the template's HTML. We could generate dynamically the HTML to send back to the browser in the UserEvent of the SummaryProcessor Web item. However, because the format of this page will be static, with only its content changing, we're using a template Web item with certain areas replaced as each whitepaper is requested. The template will display the whitepaper name and (obviously) the summary, so we insert placeholder tags into the template at design time, which are replaced at run time. Our template contains the following HTML code at a place where we want the whitepaper name to be displayed in the browser:

VB6 Book Sample Web application
<WC@WHITEPAPERNAME> Whitepaper name </WC@WHITEPAPERNAME>
Summary

When the WriteTemplate method for this Web item is called, the WC@WHITEPAPERNAME tags in the template trigger the ProcessTag event of the template. This event is fired once for each pair of tags, which allows the text between the tags ("Whitepaper Name" in this case) to be substituted by whatever text is required. The same pair of tags is used throughout the template in areas where the whitepaper name is required. The whitepaper summary is replaced using the same technique. Tags are recognized as replacement tags because of their starting prefix; the TagPrefix is a property of each Web item, which is set to WC@ by default. If this prefix is changed, the new prefix should contain at least one character that is unlikely to appear elsewhere in the HTML generated by the Web item. This reduces the risk of ordinary HTML or text being processed as a replacement tag. The code used to perform the replacements is quite simple:

'Depending on the tag being processed, substitute either the whitepaper
'name or summary.
Select Case LCase$(TagName)
    Case "wc@whitepapername"
        TagContents = smWhitepaperName

    Case "wc@whitepapersummary"
        TagContents = smWhitepaperSummary
End Select

Both TagName and TagContents are passed into the event. The two variables used to replace content are set in the SummaryProcessor_UserEvent event procedure that is the target of all the whitepaper hyperlinks in the top frame of the Summary page.

Now we're at the point where we can display summaries of our whitepapers to users of our application, but we haven't included a mechanism for displaying the full text of our whitepapers. This can easily be done by including a hyperlink (in the form of a button, perhaps), which will request the file containing the whitepaper. As we've already stated, we want users to register (or sign in if they have previously registered) before we allow them access to the full text of our whitepapers. This being the case, we should probably display a message to that effect if the user requests a whitepaper summary without having signed in. Because we are already using text replacement tags in the SummaryView template, we'll use the same technique to place either a hyperlink button or a message in the whitepaper summary section of the page.

OK, so we know what we're going to make available as replacement text in the ProcessTag event of the SummaryView Web item, but how are we going to decide what text to supply? By adding to the functionality of our CUser class, we expose a SignInUser method and SignedIn and FailureReason properties. This method and these properties let us sign in a user to the application, find out the reason for failure if they were not accepted (as shown in Figure 3-8), and also check to see if they have already been signed in.

Figure 3-8 Failed user sign-in

To make the information from the CUser object available throughout the Web class, we define an instance of the CUser class at the Web class level. We create the CUser object in the Web class Initialize event (and to be tidy, destroy it in the Terminate event) and set and query its properties and methods throughout the Web class, right? If we try this approach, what happens?

Well, we can sign in to the application and be taken to the whitepaper summary page without a problem. However, when we display a whitepaper summary, we find that the information from our sign-in has disappeared and that we are asked to sign in before being able to view the full text of our whitepaper. The problem we're facing is one of application state. Unless we do something to prolong the application lifetime, each request the browser sends to the server causes an instance of the application to be created when the HTTP request is received, and causes that instance of the application to be destroyed once the HTTP request has been completed. Thus, during the sign-in process, our CUser object accepts the sign-in and sets its SignedIn property to True. When the request arrives to display a summary article, the previous instance of CUser is long gone and we access a new instance that has no knowledge of our previous sign-in. What can we do about this?

The easiest change we can make is to set the StateManagement property of the Web class to keep the Web class alive between requests. StateManagement can be set to wcNoState (the default) so that the Web class is continually created and destroyed as requests come in from the browser. The wcRetainInstance setting, however, keeps the Web class alive between requests. Once the StateManagement property of the Web class is set to wcRetainInstance, we can sign in to the application, choose a whitepaper summary, and be presented with the opportunity to view the full whitepaper. If we look at the temporary ASP file created when we start a debugging session of our application, we can see how this property change has affected the file:

<%
Server.ScriptTimeout=600
Response.Buffer=True
Response.Expires=0

If (VarType(Application("~WC~WebClassManager")) = 0) Then
    Application.Lock
    If (VarType(Application("~WC~WebClassManager")) = 0) Then
        Set Application("~WC~WebClassManager") = _
            Server.CreateObject("WebClassRuntime.WebClassManager")
    End If
    Application.UnLock
End If

Application("~WC~WebClassManager").ProcessRetainInstanceWebClass _
    "BookWebApp.BookWebClass", _
    "~WC~BookWebApp.BookWebClass", _
    Server, _
    Application, _
    Session, _
    Request, _
    Response
%>

The difference is that the ProcessRetainInstanceWebClass method of the WebClassManager object is used instead of the ProcessNoStateWebClass method. The ProcessRetainInstanceWebClass method has an additional parameter ("~WC~BookWebApp.BookWebClass"), which is the name of the session that should be kept alive. Subsequent accesses to the application by the same user utilize the existing Web class instance by retrieving a reference to it using the same session name. Using this mechanism to maintain information between requests relies on the browser accepting cookies from the server. The server sends the name for the session as a cookie to the browser when it first accesses the application. Subsequently, whenever the browser accesses the application (until the user terminates the browser), it passes the session name cookie back to the Web server. This is how the server knows which session to associate with the request. If the user of the application has set his or her browser to refuse cookies, no session name information can be passed as a cookie. In this situation, even though we've set the StateManagement property of the Web class to wcRetainInstance, the server will generate a new session for the browser because it has no information that the browser has previously established a session on the server.

The same problem arises if we use the Session object directly from within our application—the browser cannot be associated with a session without accepting a cookie. If the Session object is available to us, we could store a user object in it to maintain our sign-in information. Alternatively, we could simply store a Boolean that gave the SignedIn state in the Session object.

We can read and write cookies directly from our application by using the Cookies collections of the Response and Request objects. We can control the existence of the cookies we write by setting the Expires property. This means that, unlike the Session object, we can keep information alive after the browser has been terminated so that the information can be used the next time the browser is started and our application accessed. Cookies that stay alive like this are stored permanently (until they expire, that is) on the user's hard drive, so even if the user has accepted a cookie, there is no absolute guarantee that it will still be there the next time the browser program is run.

Another mechanism that can be used to transfer data back and forth between the server and browser is to set and query the URLData property of the Web class. This property works by storing extra information with URLs written in pages that are sent out to the browser. If any of these URLs are activated in the browser and initiate a request on the server, the URLData property of the Web class is set to reflect the data that is sent back.


Conclusion, Chapter 3

Conclusion

We've investigated many of the features available to Visual Basic developers who want to (or have to) develop Web applications. In the process, we've produced the skeleton of a functioning application—nothing to set the world on fire, but it's a start nonetheless.

This IIS Application template allows Visual Basic developers to use existing skills to create Web applications. However, to really start developing Web applications, developers should have a good knowledge of HTML, even if they don't have to write the code that is displayed in browsers. A knowledge of HTML will help Visual Basic developers get the most of the limited interaction between browser and server. Web developers also need to understand and control the lifetime of their applications and the data they rely on for operation. This definitely needs more careful consideration than for a standard Visual Basic project. Knowledge of ASP will also stand Web developers in good stead, since Web applications created with Visual Basic are based on this technology. The core objects within a Web class (Request and Response) are ASP objects, which is certainly a good reason to learn about ASP.

To produce powerful Web applications, the developers will almost certainly have to move away from a purely server-based application with the thinnest of clients and incorporate client-side scripting, DHTML, ActiveX components, and so forth, in their overall application code. This adds even more to the areas of knowledge that Visual Basic developers must enter.

No doubt that Visual Basic 6, with its IIS Application template, will kick start the development of many Web-based applications.


Chapter 4 -- Programming With Variants

 Chapter 4

Programming With Variants

JON BURN

Jon has been programming with Microsoft Windows since the mid-1980s. Originally working with C, he now uses Visual Basic for all his programming tasks. He has worked on retail software, such as the PagePlus DTP package, and a lot of other custom software in the corporate environment. Jon has also taught programming and written various articles about it. He is currently working on graphics software for business presentations.

Microsoft Visual Basic 6 further enhances the Variant data type from the previous version so that it can now hold user-defined types (UDTs). This creates yet another reason why you should become familiar with Variants and what they can do. In this chapter I will take an in-depth look at Variants and discuss the benefits and pitfalls of programming with them.


Overview of Variants

Variants were first introduced in version 2 of Visual Basic as a flexible data type that could hold each of the simple data types. The Variant data type was extended substantially with version 4 to include Byte, Boolean, Error, Objects, and Arrays, and a little further with version 5 to include the Decimal data type. The Decimal data type was the first data type that was not available as a "first class" data type—it is available only within a Variant—and you cannot directly declare a variable as a Decimal.

In Visual Basic 6, UDTs have been added to the list, effectively completing the set. Now a Variant can be assigned any variable or constant, whatever the type.

A variety of functions convert to these subtypes and test for these subtypes. Table 4-1 shows the development of the Variant data type through the versions of Visual Basic, along with the matching functions.

Table 4-1 The Evolution of Variants
Type | Visual Basic Name | Visual Basic Version | Convert Function | Test Function
0 | Empty | 2 | = Empty | IsEmpty
1 | Null | 2 | = Null | IsNull
2 | Integer | 2 | CInt | IsNumeric*
3 | Long | 2 | CLng | IsNumeric
4 | Single | 2 | CSng | IsNumeric
5 | Double | 2 | CDbl | IsNumeric
6 | Currency | 2 | CCur | IsNumeric
7 | Date | 2 | CVDate/CDate | IsDate
8 | String | 2 | CStr |
9 | Object | 4 |  | IsObject
10 | Error | 4 | CVErr | IsError
11 | Boolean | 4 | CBool |
12 | Variant | 4 | CVar |
13 | Data Object | 4 |  |
14 | Decimal | 5 | CDec | IsNumeric
17 | Byte | 4 | CByte |
36 | UDT | 6 |  |
8192 | Array | 4 | Array | IsArray
16384 | ByRef | Never? |  |

*Strictly speaking, IsNumeric tests to see if a variable can be converted to a numeric value, and is not simply reporting on a Variant's subtype.


Internal Structure

A Variant always takes up at least 16 bytes of memory and is structured as shown in Figure 4-1.

Figure 4-1 The structure of a Variant

The first two bytes correspond to the value returned by the VarType function. (The VarType return values are defined as constants in the VbVarType enumeration.) For example, if the VarType is 2 (the value of the constant vbInteger), the Variant has a subtype of Integer. You cannot change this value directly, but the conversion functions (such as CInt) will do this for you.

The Reserved bytes have no documented function yet; their principal purpose is to pad the structure out to 16 bytes. The Data area holds the value of the variable, if the value fits into 8 bytes; otherwise, the Data area holds a pointer to the data (as with strings and so on). The type indicates how the Data portion of the Variant is to be understood or interpreted.

In this way, Variants are self-describing, meaning they contain within them all the information necessary to use them.


Using Variants Instead of Simple Data Types

In this section I'll discuss the pros and cons of using Variants in place of simple data types such as Integer, Long, Double, and String. This is an unorthodox practice—the standard approach is to avoid the use of Variants for a number of reasons. We'll look at the counterarguments first.

Performance Doesn't Matter

Every journal article on optimizing Visual Basic includes a mention of how Variants are slower than underlying first-class data types. This should come as no surprise. For example, when iterating through a sequence with a Variant of subtype Integer, the interpreted or compiled code must decode the structure of the Variant every time the code wants to use its integer value, instead of accessing an integer value directly. There is bound to be an overhead to doing this.

Plenty of authors have made a comparison using a Variant as a counter in a For loop, and yes, a Variant Integer takes about 50 percent more time than an Integer when used as a loop counter. This margin decreases as the data type gets more complex, so a Variant Double is about the same as a Double, whereas, surprisingly, a Variant Currency is quicker than a Currency. If you are compiling to native code, the proportions can be much greater in certain cases.

Is this significant? Almost always it is not. The amount of time that would be saved by not using Variants would be dwarfed by the amount of time spent in loading and unloading forms and controls, painting the screen, talking to databases, and so on. Of course, this depends on the details of your own application, but in most cases it is highly unlikely that converting local variables from Variants to Integers and Strings will speed up your code noticeably.

When optimizing, you benefit by looking at the bigger picture. If your program is too slow, you should reassess the whole architecture of your system, concentrating in particular on the database and network aspects. Then look at user interface and algorithms. If your program is still so locally computation-intensive and time-critical that you think significant time can be saved by using Integers rather than Variants, you should be considering writing the critical portion in C++ and placing this in a DLL.

Taking a historical perspective, machines continue to grow orders of magnitude faster, which allows software to take more liberties with performance. Nowadays, it is better to concentrate on writing your code so that it works, is robust, and is extensible. If you need to sacrifice efficiency in order to do this, so be it—your code will still run fast enough anyway.

Memory Doesn't Matter

A common argument against Variants is that they take up more memory than do other data types. In place of an Integer, which normally takes just 2 bytes of memory, a Variant of 16 bytes is taking eight times more space. The ratio is less, of course, for other underlying types, but the Variant always contains some wasted space.

The question is, as with the issue of performance in the previous section, how significant is this? Again I think not very. If your program has some extremely large arrays—say, tens of thousands of integers—an argument could be made to allow Integers to be used. But they are the exception. All your normal variables in any given program are going to make no perceptible difference whether they are Variants or not.

I'm not saying that using Variants improves performance or memory. It doesn't. What I'm saying is that the effect Variants have is not a big deal—at least, not a big enough deal to outweigh the reasons for using them.

Type Safety

A more complex argument is the belief that Variants are poor programming style—that they represent an unwelcome return to the sort of dumb macro languages that encouraged sloppy, buggy programming.

The argument maintains that restricting variables to a specific type allows various logic errors to be trapped at compile time, an obviously good thing. Variants, in theory, take away this ability.

To understand this issue fully we must first look at the way non-Variant variables behave. In the following pages I have split this behavior into four key parts of the language, and have contrasted how Variants behave compared to simple data types in each of these four cases:

  • Assignment

  • Function Calls

  • Operators and Expressions

  • Visual Basic Functions

Case 1: Assignment between incompatible variables

Consider the following code fragment (Example A):

Dim i As Integer, s As String
s = "Hello"
i = s

What happens? Well, it depends on which version of Visual Basic you run. In pre-OLE versions of Visual Basic you got a Type mismatch error at compile time. In Visual Basic 6, there are no errors at compile time, but you get the Type mismatch trappable error 13 at run time when the program encounters the i = s line of code.

NOTE

Visual Basic 4 was rewritten using the OLE architecture; thus, versions 3 and earlier are "pre-OLE."

The difference is that the error occurs at run time instead of being trapped when you compile. Instead of you finding the error, your users do. This is a bad thing.

The situation is further complicated because it is not the fact that s is a String and i is an Integer that causes the problem. It is the actual value of s that determines whether the assignment can take place.

This code succeeds, with i set to 1234 (Example B):

Dim i As Integer, s As String
s = "1234"
i = s

This code in Example C does not succeed (you might have thought that i would be set to 0, but this is not the case):

Dim i as Integer, s As String
s = ""
i = s

These examples demonstrate why you get the error only at run time. At compile time the compiler cannot know what the value of s will be, and it is the value of s that decides whether an error occurs.

The behavior is exactly the same with this piece of code (Example D):

Dim i As Integer, s As String
s = ""
i = CInt(s)

As in Example C, a type mismatch error will occur. In fact, Example C is exactly the same as Example D. In Example C, a hidden call to the CInt function takes place. The rules that determine whether CInt will succeed are the same as the rules that determine whether the plain i = s will succeed. This is known as implicit type conversion, although some call it "evil" type coercion.

The conversion functions CInt, CLng, and so on, are called implicitly whenever there is an assignment between variables of different data types. The actual functions are implemented within the system library file OLEAUT32.DLL. If you look at the exported functions in this DLL, you'll see a mass of conversion functions. For example, you'll see VarDecFromCy to convert a Currency to a Decimal, or VarBstrFromR8 to convert a string from an 8-byte Real, such as a Double. The code in this OLE DLL function determines the rules of the conversion within Visual Basic.

If the CInt function had worked the same way as Val does, the programming world would've been spared a few bugs (Example E).

Dim i As Integer, s As String
s = ""
i = Val(s)

This example succeeds because Val has been defined to return 0 when passed the empty string. The OLE conversion functions, being outside the mandate of Visual Basic itself, simply have different rules (Examples F and G).

Dim i As Integer, s As String
s = "1,234"
i = Val(s)

Dim i As Integer, s As String
s = "1,234"
i = CInt(s)

Examples F and G also yield different results. In Example F, i becomes 1, but in Example G, i becomes 1234. In this case the OLE conversion functions are more powerful in that they can cope with the thousands separator. Further, they also take account of the locale, or regional settings. Should your machine's regional settings be changed to German standard, Example G will yield 1 again, not 1234, because in German the comma is used as the decimal point rather than as a thousands separator. This can have both good and bad side effects.

These code fragments, on the other hand, succeed in all versions of Visual Basic (Examples H and I):

Dim i As Variant, s As Variant
s = "Hello"
i = s

Dim i As Variant, s As Variant
s = "1234"
i = s

In both the above cases, i is still a string, but why should that matter? By using Variants throughout our code, we eliminate the possibility of type mismatches during assignment. In this sense, using Variants can be even safer than using simple data types, because they reduce the number of run-time errors. Let's look now at another fundamental part of the syntax and again contrast how Variants behave compared to simple data types.

LOCALE EFFECTS

Suppose you were writing a little calculator program, where the user types a number into a text box and the program displays the square of this number as the contents of the text box change.

Private Sub Text1_Change()
    If IsNumeric(Text1.Text) Then
        Label1.Caption = Text1.Text * Text1.Text
    Else
        Label1.Caption = ""
    End If
End Sub

Note that the IsNumeric test verifies that it is safe to multiply the contents of the two text boxes without fear of type mismatch problems. Suppose "1,000" was typed into the text box—the label underneath would show 1,000,000 or 1, depending on the regional settings. On the one hand, it's good that you get this international behavior without performing any extra coding, but it could also be a problem if the user was not conforming to the regional setting in question. Further, to prevent this problem, if a number is to be written to a database or file, it should be written as a number without formatting, in case it is read at a later date on a machine where the settings are different.

Also, you should also avoid writing any code yourself that parses numeric strings. For example, if you were trying to locate the decimal point in a number using string functions, you might have a problem:

InStr(53.6, ".")

This line of code will return 3 on English/American settings, but 0 on German settings.

Note, finally, that Visual Basic itself does not adhere to this convention in its own source code. The number 53.6 means the same whatever the regional settings. We all take this for granted, of course.

Case 2: Function parameters and return types

Consider the following procedure:

Sub f(ByVal i As Integer, ByVal s As String)
End Sub

This procedure is called by the following code:

Dim i As Integer, s As String
s = "Hello"
i = 1234

Call f(s, i)

You'll notice I put the parameters in the wrong order.

With pre-OLE versions of Visual Basic you get a Parameter Type Mismatch error at compile time, but in Visual Basic 4, 5, and 6 the situation is the same as in the previous example—a run-time type mismatch, depending on the value in s, and whether the implicit CInt could work.

Instead, the procedure could be defined using Variants:

Sub f(ByVal i As Variant, ByVal s As Variant)
End Sub

The problem is that you might reasonably expect that after assigning 6.4 to x in the procedure subByRef, which is declared in the parameter list as a Variant, Debug.Print would show 6.4. But instead it shows only 6.

Now no run-time errors or compile-time type mismatch errors occur. Of course, it's not necessarily so obvious by looking at the declaration what the parameters mean, but then that's what the parameter name is for.

Returning to our survey of how Variants behave compared to simple data types, we now look at expressions involving Variants.

Case 3: Operators

I have already suggested, for the purposes of assignment and function parameters and return values, that using Variants cuts down on problematic run-time errors. Does this also apply to the use of Visual Basic's own built-in functions and operators? The answer is, "It depends on the operator or function involved."

Arithmetic operators All the arithmetic operators (such as +, -, *, \, /, and ^) evaluate their parameters at run time and throw the ubiquitous type mismatch error if the parameters do not apply. With arithmetic operators, there is neither an advantage nor a disadvantage to using Variants instead of simple data types; in either case, it's the value, not the data type, that determines whether the operation can take place. In Example A, we get type mismatch errors on both lines:

Dim s As String, v As Variant
s = "Fred"
v = "Fred"
s = s - s
v = v - v

But in Example B, these lines both succeed:

Dim s As String, v As Variant
s = "123"
v = "123"
s = s - s
v = v - v

A lot of implicit type conversion is going on here. The parameters of "-" are converted at run time to Doubles before being supplied to the subtraction operator itself. CDbl("Fred") does not work, so both lines in Example A fail. CDbl("123") does work, so the subtraction succeeds in both lines of Example B.

There is one slight difference between v and s after the assignments in Example B: s is a string of length 1 containing the value 0, while v is a Variant of subtype Double containing the value 0. The subtraction operator is defined as returning a Double, so 0 is returned in both assignments. This is fine for v - v, which becomes a Variant of subtype Double, with value 0. On the other hand, s is a string, so CStr is called to convert the Double value to 0.

All other arithmetic operators behave in a similar way to subtraction, with the exception of +.

Option "Strict Type Checking"

Some other authors have argued for the inclusion of another option along the lines of "Option Explicit" that would enforce strict type checking. Assignment between variables of different types would not be allowed and such errors would be trapped at compile time. The conversion functions such as CInt and CLng would need to be used explicitly for type conversion to take place.

This would effectively return the Visual Basic language to its pre-OLE style, and Examples A, B, and C would all generate compile-time errors. Example D would still return a run-time type mismatch, however.

Examples E, F, and G would succeed with the same results as above. In other words, code using Variants would be unaffected by the feature.

Comparison operators We normally take the comparison operators (such as <, >, and =) for granted and don't think too much about how they behave. With Variants, comparison operators can occasionally cause problems.

The comparison operators are similar to the addition operator in that they have behavior defined for both numeric and string operands, and unfortunately this behavior is different.

A string comparison will not necessarily give the same result as numeric comparison on the same operands, as the following examples show:

Dim a, b, a1, b1
a = "1,000"
b = "500"
a1 = CDbl(a)
b1 = CDbl(b)
' Now a1 > b1 but a < b

Notice also that all four variables—a, b, a1, and b1—are numeric in the sense that IsNumeric will return True for them.

As with string and number addition, the net result is that you must always be aware of the potential bugs here and ensure that the operands are converted to a numeric or string subtype before the operator is used.

Case 4: Visual Basic's own functions

Visual Basic's own functions work well with Variants, with a few exceptions. I won't cover this exhaustively but just pick out some special points.

The Visual Basic mathematical functions works fine with Variants because they each have a single behavior that applies only to numerics, so there is no confusion. In this way, these functions are similar to the arithmetic operators. Provided the Variant passes the IsNumeric test, the function will perform correctly, regardless of the underlying subtype.

a =  Hex("1,234")
a = Log("1,234")
'etc.. No problems here

Type mismatch errors will be raised should the parameter not be numeric.

The string functions do not raise type mismatch errors, because all simple data types can be converted to strings (for this reason there is no IsString function in Visual Basic). Thus, you can apply the string functions to Variants with numeric subtypes—Mid, InStr, and so forth all function as you would expect. However, exercise extreme caution because of the effect regional settings can have on the string version of a numeric. (This was covered earlier in the chapter.)

The function Len is an interesting exception, because once again it has different behavior depending on what the data type of the parameter is. For simple strings Len returns the length of the string. For simple nonstring data Len returns the number of bytes used to store the variable. However, less well known is the fact that for Variants, it returns the length of the Variant as if it were converted to a string, regardless of the Variant's actual subtype.

Dim v As Variant, i As Integer
i = 100
v = i
' The following are now true:
' Len(i) = 2
' Len(v) = 3

This provides one of the only ways of distinguishing a simple Integer variable from a Variant of subtype Integer at run time.

Flexibility

Some time ago, while I was working for a big software house, I heard this (presumably exaggerated) anecdote about how the company had charged a customer $1 million to upgrade the customer's software. The customer had grown in size, and account codes required five digits instead of four. That was all there was to it. Of course, the client was almost certainly being ripped off, but there are plenty of examples in which a little lack of foresight proves very costly to repair. The Year 2000 problem is a prime example. It pays to allow yourself as much flexibility and room for expansion that can be reasonably foreseen. For example, if you need to pass the number of books as a parameter to a function, why only allow less than 32,768 books (the maximum value of an Integer)? You might also need to allow for half a book too, so you wouldn't want to restrict it to Integer or Long. You'd want to allow floating-point inputs. You could at this point declare the parameter to be of type Double because this covers the range and precision of Integer and Long as well as handling floating points. But even this approach is still an unnecessary restriction. Not only might you still want the greater precision of Currency or Decimal, you might also want to pass in inputs such as An unknown number of books.

The solution is to declare the number of books as a Variant. The only commitment that is made is about the meaning of the parameter—that it contains a number of books—and no restriction is placed on that number. As much flexibility as possible is maintained, and the cost of those account code upgrades will diminish.

Function ReadBooks(ByVal numBooks As Variant)
    ' Code in here to read books
End Function

Suppose we want to upgrade the function so that we can pass An unknown number of books as a valid input. The best way of doing this is to pass a Variant of subtype Null. Null is specifically set aside for the purpose of indicating not known.

If the parameter had not been a Variant, you would have had some choices:

  • Add another parameter to indicate that the number is unknown. A drawback of this approach is that a modification would be required everywhere this function is called. That way lies the million-dollar upgrade. If the parameter were Optional, you would get away with this approach, but only the first time.

  • Allow a special value to indicate unknown—perhaps -1 or maybe 32768. We might create a constant of this value so that the code reads a little better—Const bkNotKnown = -1—and use that. This approach leads to bugs. Sooner or later, you or another programmer will forget that -1 is reserved and use it as an ordinary value of number of books, however unlikely that may seem at the time you choose the value of the constant.

If the parameters are Variants, you avoid these unsatisfactory choices when modifying the functions. In the same way, parameters and return types of class methods, as well as properties, should all be declared as Variants instead of first-class data types.

HUNGARIAN NOTATION

The portion of Hungarian notation that refers to data type has little relevance when programming with Variants. Indeed, as variables of different data types can be freely assigned and interchanged, the notation has little relevance in Visual Basic at all.

I still use variable prefixes, but only to assist in the categorization of variables at a semantic level. So, for example, "nCount" would be a number that is used as a counter of something. The n in this instance stands for a general numeric, not an Integer.

Defensive Coding

I have extolled the virtues of using Variants and the flexibility that they give. To be more precise, they allow the interface to be flexible. By declaring the number of books to be a Variant, you make it unlikely that the data type of that parameter will need to be modified again.

This flexibility of Variants has a cost to it. What happens if we call the function with an input that doesn't make sense?

N = ReadBooks("Ugh")

Inside the function, we are expecting a number—so what will it make of this? If we are performing some arithmetic operations on the number, we risk a type mismatch error when a Variant with these contents is passed. You must assert your preconditions for the function to work. If, as in this instance, the input must be numeric, be sure that this is the case:

Function ReadBooks(ByVal input As Variant) As Variant
    If IsNumeric(input) Then
        ' Do stuff, return no error
    Else
        ' Return error
    End If
End Function

In other words, you code defensively by using the set of Is functions to verify that a parameter is suitable for the operation you're going to perform on it.

You might think about using Debug.Assert in this instance, but it is no help at run time because all the calls to the Assert method are stripped out in compilation. So you would still need to implement your own checks anyway.

Of course, verifying that your input parameter is appropriate and satisfies the preconditions is not just about checking the type. It would also involve range checks, ensuring that we are not dividing by 0, and so on.

Is this feasible? In practice, coding defensively like this can become a major chore, and it is easy to slip up or not bother with it. It would be prudent if you were writing an important1 piece of component code, especially if the interface is public, to place defensive checks at your component entry points. But it is equally likely that a lot of the time you will not get around to this.

What are the consequences of not performing the defensive checks? While this naturally depends on what you are doing in the function, it is most likely that if there is an error it will be a type mismatch error. If the string Ugh in the previous example was used by an operator or built-in function that only worked with numerics, a type mismatch would occur. Interestingly, had the parameter to ReadBooks been declared as a Double instead of a Variant, this same error would be raised if the string Ugh was passed.

The only difference is that in the case of the Variant the error is raised within the function, not outside it. You have the choice of passing this error back to the calling client code or just swallowing the error and carrying on. The approach you take will depend on the particular circumstances and your preferences.

Using the Variant as a General Numeric Data Type

Don't get sidetracked by irrelevant machine-specific details. Almost all the time, we want to deal with numbers. For example, consider your thought process when you choose between declaring a variable to be of type Integer or type Long. You might consider what the likely values of the variable are going to be, worry a little bit about the effect on performance or memory usage, and maybe check to see how you declared a similar variable elsewhere so that you can be consistent. Save time—get into the habit of declaring all these variables as Variants.

NOTE

All variables in my code are either Variants or references to classes. Consequently, a lot of code starts to look like this.

Dim Top As Variant
Dim Left As Variant
Dim Width As Variant
Dim Height As Variant
After a time I started to take advantage of the fact that Variants are the default, so my code typically now looks like this:
Dim Top, Left, Width, Height
I see no problem with this, but your current Visual Basic coding standards will more than likely prohibit it. You might think about changing them.

VARIANT BUGS WHEN PASSING PARAMETERS BY REFERENCE

Variants do not always work well when passed by reference, and can give rise to some hard-to-spot bugs. The problem is illustrated in the following example:

Private Sub Form_Load()
    Dim i As Integer
    i = 3
    subByVal i
    subByRef i
End Sub

Private Sub subByVal(ByVal x As Variant)
    x = 6.4
    Debug.Print x     'shows 6.4
End Sub

Private Sub subByRef(x As Variant)
    x = 6.4
    Debug.Print x     'shows 6
End Sub

Notice that the only difference between the procedures subByVal and subByRef is that the parameter is passed ByVal in subByVal and ByRef in subByRef. When subByVal is called, the actual parameter i is of type Integer. In subByVal, a new parameter x is created as a Variant of subtype Integer, and is initialized with the value 3. In other words, the subtype of the Variant within the procedure is defined by the type of the variable that the procedure was actually called with. When x is then set to a value of 6.4, it converts to a Variant of subtype Double with value 6.4. Straightforward.

When subByRef is called, Visual Basic has a bit more of a problem. The Integer is passed by reference, so Visual Basic cannot allow noninteger values to be placed in it. Instead of converting the Integer to a Variant, Visual Basic leaves it as an Integer. Thus, even in the procedure subByRef itself, where x is declared as a Variant, x is really an Integer. The assignment of x = 6.4 will result in an implicit CInt call and x ends up with the value 6. Not so straightforward.

Procedures like subByVal are powerful because they can perform the same task, whatever the data type of the actual parameters. They can even perform different tasks depending on the type of the actual parameter, though this can get confusing.

Procedures like subByRef lead to bugs—avoid them by avoiding passing by reference.


Using Variants Instead of Objects

Earlier in the chapter, I extolled the use of Variants in the place of simple data types like Integer and String. Does the same argument apply for objects?

Put simply, the answer is no, because there is considerable extra value added by declaring a variable to be of a specific object type. Unlike the simple data types, we can get useful compile-time error messages that help prevent bugs. If the Variant (or Object) data type was used, these errors would surface only at run time—a bad thing.

By way of explanation, consider the following simple example. In this project there is one class, called Cow, which has few properties, such as Age, TailLength, and so forth. We then create a routine

Private Sub AgeMessage(c As Cow)
    MsgBox c.Age
End Sub

If you accidentally misspell Age and instead type

MsgBox c.Agg

provided c is declared as Cow, you will receive a compile-time error message so that you can correct it. If the parameter was declared as a Variant (or Object), Visual Basic cannot know whether there is a legitimate property of c called Agg until, at run time, it actually knows what the object is. Hence, all you get is a run-time error 438 instead.

Notice how this argument does not apply back to simple data types. Although simple data types do not have properties, they do have certain operators that may or may not be well defined for them. However, a piece of code such as this

Dim s As String
s = s * s

where the * operator is undefined for strings, will result in a run-time type mismatch, not a compile-time error. So the advantage of not declaring as Variant is lost.


Other Variant Subtypes

Flexibility is the fundamental reason to use Variants. But the built-in flexibility of Variants is not advertised enough, and consequently they tend to be underused. The use of Empty, Null, and Variant arrays—and now in version 6, UDTs—remain underused in the Visual Basic programmer community.

Empty and Null

Any uninitialized Variant has the Empty value until something is assigned to it. This is true for all variables of type Variant, whether Public, Private, Static, or local. This is the first feature to distinguish Variants from other data types—you cannot determine whether any other data type is uninitialized.

As well as testing for VarType zero, a shorthand function exists—IsEmpty—which does the same thing but is more readable.

In early versions of Visual Basic, once a Variant was given a value, the only way to reset it to Empty was to assign it to another Variant that itself was empty. In Visual Basic 5 and 6, you can also set it to the keyword Empty, as follows:

v1 = Empty

I like Empty, although I find it is one of those things that you forget about and sometimes miss opportunities to use. Coming from a C background, where there is no equivalent, isn't much help either. But it does have uses in odd places, so it's worth keeping it in the back of your mind. File under miscellaneous.

Of course, Null is familiar to everyone as that database "no value" value, found in all SQL databases. But as a Variant subtype it can be used to mean no value or invalid value in a more general sense—in fact, in any sense that you want to use it. Conceptually, it differs from Empty in that it implies you have intentionally set a Variant to this value for some reason, whereas Empty implies you just haven't gotten around to doing anything with the Variant yet.

As with Empty, you have an IsNull function and a Null keyword that can be used directly.

Visual Basic programmers tend to convert a variable with a Null value—read, say from a database—to something else as quickly as possible. I've seen plenty of code where Null is converted to empty strings or zeros as soon as it's pulled out of a recordset, even though this usually results in information loss and some bad assumptions. I think this stems from the fact that the tasks we want to perform with data items—such as display them in text boxes or do calculations with them—often result in the all too familiar error 94, "Invalid use of Null."

This is exacerbated by the fact that Null propagates through expressions. Any arithmetic operator (+, -, *, /, \, Mod, ^) or comparison operator (<, >, =, <>) that has a Null as one of its operands will result in a Null being the value of the overall expression, irrespective of the type or value of the other operand. This can lead to some well-known bugs, such as:

v = Null
If v = Null Then
    MsgBox "Hi"
End if

In this code, the message "Hi" will not be displayed because as v is Null, and = is just a comparison operator here, the value of the expression v = Null is itself Null. And Null is treated as False in If...Then clauses.

The propagation rule has some exceptions. The string concatenation operator & treats Null as an empty string "" if one of its operands is a Null. This explains, for example, the following shorthand way of removing Null when reading values from a database:

v = "" & v

This will leave v unchanged if it is a string, unless it is Null, in which case it will convert it to "".

Another set of exceptions is with the logical operators (And, Eqv, Imp, Not, Or, Xor). Here Null is treated as a third truth value, as in standard many-valued logic. Semantically, Null should be interpreted as unsure in this context, and this helps to explain the truth tables. For example:

v = True And Null

gives v the value Null, but

v = True Or Null

gives v the value True. This is because if you know A is true, but are unsure about B, then you are unsure about A and B together, but you are sure about A or B. Follow?

By the way, watch out for the Not operator. Because the truth value of Null lies halfway between True and False, Not Null must evaluate to Null in order to keep the logical model consistent. This is indeed what it does.

v = Not Null
If IsNull(v) Then MsgBox "Hi"  ' You guessed it...

That's about all on Null—I think it is the trickiest of the Variant subtypes, but once you get to grips with how it behaves, it can add a lot of value.

Arrays

Arrays are now implemented using the OLE data type named SAFEARRAY. This is a data type that, like Variants and classes, allows arrays to be self-describing. The LBound and number of elements for each dimension of the array are stored in this structure. Within the inner workings of OLE, all access to these arrays is through an extensive set of API calls implemented in the system library file OLEAUT32.DLL. You do not get or set the array elements directly, but you use API calls. These API calls use the LBound and number of elements to make sure they always write within the allocated area. This is why they are safe arrays—attempts to write to elements outside the allowed area are trapped within the API and gracefully dealt with.2

The ability to store arrays in Variants was new to Visual Basic 4, and a number of new language elements were introduced to support them such as Array and IsArray.

To set up a Variant to be an array, you can either assign it to an already existing array or use the Array function. The first of these methods creates a Variant whose subtype is the array value (8192) added to the value of the type of the original array. The Array function, on the other hand, always creates an array of Variants—VarType 8204 (which is 8192 plus 12).

The following code shows three ways of creating a Variant array of the numbers 0, 1, 2, 3:

Dim v As Variant
Dim a() As Integer
Dim i As Integer

' Different ways to create Variant arrays
' 1. Use the Array function
v = Array(0, 1, 2, 3) 'of little practical use
v = Empty

' 2. Create a normal array, and assign it to a Variant.
' Iterate adding elements using a normal array...
For i = 0 To 3
    ReDim Preserve a(i) As Integer
    a(i) =  i
Next i

' ...and copy array to a Variant
v = a
'or
v = a()
' but not v() = a()

v = Empty

' 3. Start off with Array, and then ReDim to preferred size
' avoiding use of intermediate array.
For i = 0 To 3
    ' First time we need to create array
    If IsEmpty(v) Then
        v = Array(i)
    Else
        ' From then on, ReDim Preserve will work on v
        ReDim Preserve v(i)
    End If
    v(i) = i
Next i

Notice that the only difference between the last two arrays is that one is a Variant holding an array of integers and the other is a Variant holding an array of Variants. It can be easy to get confused here, look at the following:

ReDim a(5) As Variant

This code is creating an array of Variants, but this is not a Variant array. What consequence does this have? Not much anymore. Before version 6 you could utilize array copying only with Variant arrays, but now you can do this with any variable-sized array.

So what is useful about placing an array in a Variant? As Variants can contain arrays, and they can be arrays of Variants, those contained Variants can themselves be arrays, maybe of Variants, which can also be arrays, and so on and so forth.

Just how deep can these arrays be nested? I don't know if there is a theoretical limit, but in practice I have tested at least 10 levels of nesting. This odd bit of code works fine:

Dim v As Variant, i As Integer

' Make v an array of two Variants, each of which is an array
' of two Variants, each of...and so on
For i = 0 To 10
    v = Array(v, v)
Next i

' Set a value...
v(0)(0)(0)(0)(0)(0)(0)(0)(0)(0)(0) = 23

How do these compare to more standard multidimensional arrays? Well, on the positive side, they are much more flexible. The contained arrays—corresponding to the lower dimensions of a multidimensional array—do not have to have the same number of elements. Figure 4-2 explains the difference pictorially.

Figure 4-2 The difference between a standard two-dimensional array (top) and a Variant array (bottom)

These are sometimes known as ragged arrays. As you can see from the diagram, we do not have all the wasted space of a multidimensional array. However you have to contrast that with the fact that the Variant "trees" are harder to set up.

This ability of Variants to hold arrays of Variants permits some interesting new data structures in Visual Basic. One obvious example is a tree. In this piece of code, an entire directory structure is folded up and inserted in a single Variant:

Private Sub Form_Load()
    Dim v As Variant
    v = GetFiles("C:\") ' Places contents of C: into v
End Sub

Public Function GetFiles(ByVal vPath As Variant) As Variant
    ' NB cannot use recursion immediately as Dir
    ' does not support it, so get array of files first
    Dim vDir As Variant, vSubDir As Variant, i

    vDir = GetDir(vPath)

    ' Now loop through array, adding subdirectory information.
    If Not IsEmpty(vDir) Then
        For i = LBound(vDir) To UBound(vDir)
            ' If this is a dir, then...
            If (GetAttr(vDir(i)) And vbDirectory) = vbDirectory Then
                ' replace dir name with the dir contents.
                vDir(i) = GetFiles(vDir(i))
            End If
        Next i
    End If

    GetFiles = vDir

End Function

Private Function GetDir(ByVal vPath As Variant) As Variant
    ' This function returns a Variant that is an array
    ' of file and directory names (not including "." or "..")
    ' for a given directory path.
    Dim vRet As Variant, fname As Variant

    ' Add \ if necessary.
    If Right$(vPath, 1) <> "\" Then vPath = vPath & "\"

    ' Call the Dir function in a loop.
    fname = Dir(vPath, vbNormal & vbDirectory)
    Do While fname <> ""
        If fname <> "." And fname <> ".." Then
            vRet = AddElement(vRet, vPath & fname)
        End If
        fname = Dir()
    Loop

    ' Return the array.
    GetDir = vRet
End Function

Public Function AddElement(ByVal vArray As Variant, _
    ByVal vElem As Variant) As Variant
    ' This function adds an element to a Variant array
    ' and returns an array with the element added to it.

    Dim vRet As Variant ' To be returned

    If IsEmpty(vArray) Then
        ' First time through, create an array of size 1.
        vRet = Array(vElem)
    Else
        vRet = vArray
        ' From then on, ReDim Preserve will work.
        ReDim Preserve vRet(UBound(vArray) + 1)
        vRet(UBound(vRet)) = vElem
    End If

    AddElement = vRet

End Function

Using + for String Concatenation

This misconceived experiment with operator overloading was considered bad form even back in the days of Visual Basic 2, when the string concatenation operator & was first introduced. Yet it's still supported in Visual Basic 6. In particular, since version 4 brought in extensive implicit type conversion between numerics and strings, this issue has become even more important. It's easy to find examples of how you can get tripped up. Can you honestly be confident of what the following will print?

Debug.Print "56" + 48
Debug.Print "56" + "48"
Debug.Print "56" - "48"

What should happen is that adding two strings has the same effect as subtracting, multiplying, or dividing two strings—that is, the addition operator should treat the strings as numeric if it can; otherwise, it should generate a type mismatch error. Unfortunately, this is not the case. The only argument for why the operator stays in there, causing bugs, is backward compatibility.

One point to note about this code is that this is an extremely efficient way of storing a tree structure, because as v is a multidimensional ragged array, the structure contains less wasted space than its equivalent multidimensional fixed-sized array. This contrasts with the accusation usually leveled at Variants, that they waste a lot of memory space.

User-Defined Types

The rehabilitation of UDTs was the biggest surprise for me in version 6 of Visual Basic. It had looked as if UDTs were being gradually squeezed out of the language. In particular, the new language features such as classes, properties, and methods did not seem to include UDTs. Before version 6, it was not possible to

  1. have a UDT as a public property of a class or form.

  2. pass a UDT as a parameter ByVal to a sub or function.

  3. have a UDT as a parameter to a public method of a class or form.

  4. have a UDT as the return type of a public method of a class or form.

  5. place a UDT into a Variant.

But this has suddenly changed and now it is possible in version 6 to perform most of these to a greater or lesser extent. In this chapter, I am really only concentrating on the last point, that of placing a UDT into a Variant.

Restrictions are imposed on the sorts of UDTs that can be placed in a Variant. They must be declared within a public object module. This rules out their use within Standard EXE programs, as these do not have public object modules. This is a Microsoft ActiveX-only feature. Internally, the Data portion of the Variant structure is always a simple pointer to an area of memory where the UDT's content is sitting. The Type is always 36. This prompts the question of where and how the meta-data describing the fields of the UDT is kept. Remember that all other Variant subtypes are self-describing, so UDTs must be, too. The way it works is that from the Variant you can also obtain an IRecordInfo interface pointer. That interface has functions that return everything you want to know about the UDT.

We are able to improve substantially on the nesting ability demonstrated earlier with Variant arrays. While it is still impossible to have a member field of a UDT be that UDT itself—a hierarchy that is commonly needed—you can use a Variant and sidestep the circular reference trap. The following code shows a simple example of an employee structure (Emp) in an imaginary, not-so-progressive organization (apologies for the lack of originality). The boss and an array of workers are declared as Variant—these will all in fact be Emps themselves. GetEmp is just a function that generates Emps.

' In Class1
Public Type Emp
    Name As Variant
    Boss As Variant
    Workers() As Variant
 End Type

' Anywhere Class1 is visible:
Sub main()
    Dim a As Emp
    a.Name = "Adam"
    a.Boss = GetEmp(1)
    a.Workers = Array(GetEmp(2), GetEmp(3))
End Sub

Private Function GetEmp(ByVal n) As Emp
    Dim x As Emp
    x.Name = "Fred" & n
    GetEmp = x
End Function

Note that this code uses the ability to return a UDT from a function. Also, the Array function always creates an array of Variants, so this code now works because we can convert the return value of GetEmp to a Variant.

Interface Inviolability

If you're like me, you may well have experienced the frustration of creating ActiveX components (in-process or out-of-process, it doesn't matter) and then realizing you need to make a tiny upgrade.

You don't want to change the interface definition because then your server is no longer compatible, the CLSID has changed, and you get into all the troublesome versioning complexity. Programs and components that use your component will all have problems or be unable to automatically use your upgraded version.

There isn't a lot you can do about this. Visual Basic imposes what is a very good discipline on us with its version compatibility checking, though it is sometimes a bitter pill to swallow.

In this respect, the flexibility gained by using Variants for properties and methods' parameters can be a great headache saver.

One drawback to this is that Visual Basic does not know at compile time the actual type of Workers, so you might write errors that will not be found until run time, such as the following:
a.Workers.qwert = 74

Accessing an invalid property like this will not be caught at compile time. This is analagous to the behavior of using Variants to hold objects described earlier. Similarly, the VarType of a.Workers is 8204—vbArray + vbVariant. Visual Basic does not know what is in this array. If we rewrote the above code like this:

' In Class1
Public Type Emp
    Name As Variant
    Boss As Variant
    Workers As Variant
 End Type

' Anywhere Class1 is visible:
Sub main()
    Dim a As Emp
    ReDim a.Workers(0 To 1) As Emp

    a.Name = "Adam"
    a.Boss = GetEmp(1)

    a.Workers(0) = GetEmp(2)
    a.Workers(1) = GetEmp(3)
End Sub

This time the VarType of a.Workers is 8228—vbArray + vbUserDefinedType. In other words, Visual Basic knows that Workers is an array of Emps, not an array of Variants. This has similarities to the late-bound and early-bound issue with objects and classes. (See "How Binding Affects ActiveX Component Performance" in the Visual Basic Component Tools Guide.) At compile time, however, the checking of valid methods and properties is still not possible because the underlying declaration is Variant.

The alternative way of implementing this code would be to create a class called Emp that had other Emps within it—I'm sure you've often done something similar to this. What I find interesting about the examples above is the similarity they have with this sort of class/object code—but no objects are being created here. We should find performance much improved over a class-based approach because object creation and deletion still take a relatively long time in Visual Basic. This approach differs slightly in that an assignment from one Variant containing a UDT to another Variant results in a deep copy of the UDT. So in the above examples, if you copy an Emp, you get a copy of all the fields and their contents. With objects, you are just copying the reference and there is still only one underlying object in existence. Using classes rather than UDTs for this sort of situation is still preferable given the many other advantages of classes, unless you are creating hundreds or thousands of a particular object. In this case, you might find the performance improvement of UDTs compelling.

MORE ON PASSING PARAMETER BY REFERENCE

You might be wondering, "Why should I avoid passing parameters by reference? It's often very useful." In many situations, passing parameters by reference is indicative of bad design. Just as using global variables is bad design but can be the easy or lazy way out, passing parameters by reference is a shortcut that often backfires at a later date.

Passing parameters by reference is a sign that you don't have the relationships between your functions correct. The mathematical model of a function is of the form:

x = f (a,b,c,..)

where the function acts on a,b,c, and so on to produce result x. Both sides of the equal sign are the same value. You can use either x or f(a,b,c,...) interchangeably.

Likewise in Visual Basic, functions can be used as components in expressions, as this example shows:

x = Sqr(Log(y))

This is not quite the case in a Visual Basic program, because the function does something in addition to returning a value. But it's still most useful to think of the return value x as the result of what that function f does. But if x contains the result, the result cannot also be in a, b, or c. In other words, only x is changed by the function. This is my simplistic conceptual model of a function, and it is at odds with the notion of passing by reference. Passing a parameter by reference often indicates one of the following:

Functions are trying to do more than one task

This is going to lead to larger functions than need be, functions that are more complex than need be, and functions that are not as useful as they could be. You should break down the functions so that each one does only one task, as in the mathematical model.

A new class needs to be defined

If the function needs to return two related values, say an X and a Y value for a coordinate, create a class or UDT to hold the object that these values relate to, and return that. If the values are not sufficiently related to be able to define a class, you are almost certainly doing too much in the one function. As an example, this

GetCenter(f As Form, ByRef X, ByRef Y)

would be better written as

Set p = GetCenter(f As Form)

where p is an object of class Point. Alternatively

p = GetCenter(f as Form)

here p is a Variant UDT.

Functions are returning some data and some related meta-data

By meta-data I mean a description of the data returned. Functions won't need to return meta-data if you use only self-describing data types. For example, functions that return an array or a single element, depending upon some argument, should return a Variant, which can hold either, and the caller can use IsArray to determine what sort of data is returned.

Functions are returning some data and an indication of the function's success

It is quite common to use the return value of a function to return True, False, or perhaps an error code. The actual data value is returned as a parameter by reference. For example, consider this code fragment:

bRet = GetFileVersion(ByRef nVersion, filename)

Here the version of filename is returned by reference, provided the file was found correctly. If the file was not found, the function will return False and nVersion will not be accurate. You have a couple of alternatives.

  • Raise errors. This has always been the Visual Basic way. (For example, Visual Basic's own Open works in this way.)

  • Return a Variant and use the CVErr function to create a Variant of subtype vbError holding the error condition. The caller can then use IsError as follows:

    nVersion = GetFileVersion(filename)

If IsError(nVersion) Then
    ' nVersion is unreliable, take some action...
Else
    ' nVersion is reliable, use it...
End If

Error raising and trapping is not to everyone's taste, so the second option might appeal to you.

Side effects when passing by reference

Bugs result when instances of parameters unexpectedly change their value. To put it at its simplest, parameters changing their value are a side effect, and side effects trip up programmers. If I call the function

Dim a, b
a = f(b)

it is immediately apparent to me that a is likely to change, and probably b will not. But I cannot guarantee that without looking at the source code for the function f.

It is particularly unfortunate that the default parameter-passing convention is ByRef, because it means that you have to do extra typing to avoid ByRef parameters. Because of backward compatibility, I don't think there's any chance of this being changed. However, it has been suggested by the Visual Basic team that they could fill in the ByRefs automatically on listback in the editor so that people can see exactly what's happening. The problem is that this would alter all old code as it is brought into the editor and cause problems with source code control.


Chapter 5 -- Developing Applications

 Chapter 5

Developing Applications in Windows CE

My Two CEnts Worth

CHRIS DE BELLOT

During his career, Chris, a TMS Developer, has worked on a number of diverse applications. Chris has a great deal of experience in the three-tier client/server arena and is experienced in object design in Microsoft Visual Basic. A key member of the TMS team, Chris is highly respected for his knowledge, opinions, and experience in the design of user interface code, in particular the design of structured and reusable code, at both the code and business level. Chris is a Microsoft Certified Professional. In his spare time, Chris is a keen commercial aviation enthusiast and enjoys nothing more than landing 747s at London Heathrow's runway 27L using one of his many flight simulators.

I grabbed a beer and settled back down in front of a screen full of Visual Basic code. I'd been working on a killer app and had a few bugs left to find. Behind the project window I could see the news just starting. "Better stop," I thought, "and catch up on what's going on in the world."

My computer is connected to the television, which has a 29-inch screen. It's great for coding because I've finally got enough space for all my windows, and I have the benefit of being able to watch the TV at the same time. This, I tell myself, helps me to concentrate! Since Microsoft Windows CE took off in a big way, a whole load of new appliances have appeared on the market all incorporating Windows CE technology. My TV is just one example. Windows CE has the ability to address quite a few gigabytes of memory and the manufacturers have taken advantage of this by putting video memory in the actual television set; the 3-D video card is also built in. This means that I don't have to have these components stuffed into my PC. The processor and other components are still inside the PC case—the TV is just an I/O device that happens to have a large screen, plenty of RAM, and loads of disk space. The latter comes in handy when I use the Internet functionality built into the Windows CE operating system.

I have lots of other Windows CE devices—a stereo, an intelligent oven that can be programmed with menus on a CD-ROM. But my favorite device is my radio alarm clock. I'm really bad at getting up in the morning, and this little gadget lets me program any sequence of events, such as turning the radio on for half an hour, then chiming every 20 minutes until I get fed up and finally get out of bed. All of this was possible using conventional technology, but when Windows CE came out, the common platform was a real incentive for manufacturers to make all those devices that were perhaps too expensive to justify building before. After all, who in their right mind would take on developing a stereo with speech recognition? Oh well, it's getting a bit late now. I think I'll try out the roast lamb program in my programmable oven. It's just a shame the oven can't prepare the ingredients as well!

OK, so I don't really use my TV as a computer screen and I don't have all those gadgets—I just made it all up. But I do predict that in five years these sorts of gadgets will be commonplace. Windows CE has the potential to create a market in which affordable electronic devices can be built to cater to any need (well, almost!), so watch this space.


What Is Windows CE?

Windows CE is simply a 32-bit, non-PC operating system. You might have seen Windows CE devices, such as the handheld PC (HPC) or the palm-sized PC—these are just some of the many applications run by the Windows CE operating system. So, although this chapter devotes much time to writing applications for the HPC, it is important to realize that these devices are only a small subset of Windows CE applications. Windows CE is designed to be a component-based operating system that can be embedded into any number of devices. Being supplied as a series of configurable components allows Windows CE to meet the stringent memory and size constraints common to many electronic devices.

If you were to compare the number of Microsoft Windows NT and Microsoft Windows 9x machines with the number of 32-bit operating systems throughout the world, you might be surprised to find that Microsoft comprises only a small percentage of the market. Windows CE is Microsoft's attempt to claim a larger percentage of the 32-bit operating system market. One of Microsoft's goals is to have the Windows CE operating system embedded into devices ranging from industrial control systems to everyday consumer devices. Bill Gates recently said at a TechEd conference: "Although Windows 98 is the big thing today, I do expect that two years from now Windows NT and Windows CE volume will be as great, or greater than, Windows 98 volume."

The Windows 9x operating systems at present outsell Windows NT and Windows CE. If Bill Gates' instincts are proven correct, the picture might look drastically different in the year 2000.

Peet Morris, director of The Mandelbrot Set (International) Limited (TMS) and one of the authors of this book, also has high hopes for the future of Windows CE. Peet's view of Windows CE is summed up by the following statement:

"I think that Windows CE is, and will become more and more, a key technology for our industry. It presents a fantastic new opportunity for developing great software and presents a new, almost unique set of challenges for developers worldwide. As the Technical Director of TMS, I'm determined that we will be a leading provider of both Windows CE-based solutions and, for the developer, the tools and technologies required to fully exploit the opportunity Windows CE presents."

The amount of interest being shown by both the electronics and developer communities is a sure sign that Windows CE is here to stay and that it will have a large impact in the marketplace. I said earlier that Windows CE is a non-PC operating system. At the time of this writing it is already possible to buy palm-sized PCs running Windows 95 with lots of memory and disk space. For programmers looking to implement applications comparable to those on the desktop, however, Windows CE is probably not the best choice of operating systems.

Target Audience

The Windows CE operating system is specifically targeted at independent hardware vendors (IHVs) and original equipment manufacturers (OEMs). Windows NT and Windows 9x are available as software packages that can be installed on a computer; however, Windows CE is designed to reside in read-only memory (ROM). It is therefore not possible for a software developer to simply buy the operating system, install it, and write software for it. The uses of Windows CE are almost unlimited in scope in the OEM and IHV markets, although some types of applications will depend on enhancements being made to the operating system even as I write.

Consider a domestic electronic device that you have at home or in your office. Chances are that the product uses custom hardware and an operating system with the code written in either assembly language or C/C++. The manufacturers might design and build their own hardware or they might buy components upon which they build. Whichever method is used to build the product, more than likely the processor has been designed specifically for the task it performs. Any change to the product's features might well involve changes to the processor unit—in short, increasing the development time and costs.

Windows CE offers the ability for a manufacturer to buy a fully customizable operating system that is based on the Microsoft Win32 API. A large base of Win32 programmers already exists; therefore, the task of programming a Windows CE device becomes that much easier. Another benefit is the amount of training and support that is available. However, the real benefits can be better understood if you consider the additional functionality available as part of the operating system.

I mentioned that Windows CE offers the ability to build a device that can be programmed using the Win32 API and standard tools like Microsoft Visual C++. However, these capabilities by themselves probably would not be enough incentive to persuade the electronics industry to switch to Windows CE. Bear in mind that the operating system is licensed and the cost of the license fee depends on configuration and volume. OEMs are used to building their own hardware and maybe even writing their own operating systems. The real incentive for using Windows CE lies in the additional components, listed below:

  • Transmission Control Protocol/Internet Protocol (TCP/IP), Point-to-Point Protocol (PPP), and Telephony API (TAPI) components offer the ability to communicate using industry standard protocols. Using these communications components, the vendor can build support for the Internet and intranets in the same way that Windows programmers do. HTML support is also a feature of the operating system. Even for an OEM, the time and costs required to build these components from scratch would more than likely be prohibitive, because the development cost would price the device out of the market.

  • IrDA is the infrared port communications protocol. Again, using standard programming techniques, the vendor can support infrared communication of complex data. This will be a popular form of communication for devices that are in mobile locations. For example, a telephone device installed in a vehicle will need some means of receiving updates to its software or data. A typical scenario might be a user downloading his or her PC contacts file onto a handheld device, and then transmitting the data from the handheld device to the telephone using an infrared link. The telephone also serves as an example of how much potential this technology has. For example, it is feasible that a Windows CE telephone could update new software via a telephone call from the desktop—wow! (Not "Windows On Windows."<g>)

  • As most Windows CE machines don't have any disk storage available to them, the object store provides intrinsic data storage methods as part of the operating system. This is covered in more detail later in this chapter; however, the object store essentially provides database, Registry, and file system functionality. The object store is accessible via the Win32 API, so utilizing this functionality is purely a software task—the vendor does not need to build any additional hardware. I should mention at this point that the Windows CE database is not a relational database—it's an indexed sequential access method (ISAM) database, meaning it allows storage of a number of fields indexed on up to four keys. Any record might contain any data in its fields, and might contain any number of fields.

    Microsoft Visual Basic, Java, and Microsoft Foundation Class run times can be incorporated into the operating system by the device manufacturer. These components are available for purchase when you buy the Windows CE license. Devices built with these components have the added advantage that the components reside, and are executed, in ROM. This is a big bonus, because, for example, the Visual Basic run time is around 600 KB in size. A Visual Basic program would not require these components to be installed again, thereby leaving more memory for the application. Corporate buyers should make note of this point, because whether these run-time components are included in the operating system is purely the manufacturer's decision. If you will be developing applications requiring any of these run times, it might pay to invest in a device with the required components built in.

    COM objects are supported by Windows CE, although at present they can be created using only C++ and must be in-process DLLs. This means that for the time being it is not possible to build Distributed COM (DCOM) objects. It is important to remember that Windows CE is a fairly new technology. Although the Windows CE desktop systems are largely compatible with the Windows 9x/NT systems, the underlying code is totally new and is optimized for the Windows CE environment. This is one of the reasons why some of the tools available have limited functionality. However, Microsoft is listening carefully to its users, so you can expect these tools to become more powerful over time.

  • In addition to the language support, Microsoft Internet Explorer can also be an integral part of the operating system. This is an invaluable addition for Web-enabled devices. I would imagine that a vast number of Windows CE devices will be Web-enabled—WebTV is just one example. Again, imagine having to write your own Web functionality totally from scratch! The scope of devices using World Wide Web features is limited only to the imagination.

Now that I've described all the benefits, it is probably easier to comprehend just what Windows CE has to offer. Indeed, the amount of interest being shown by the electronics industry is proof of its potential. The potential market for Windows CE devices is enormous. Based on the history of the electronics industry over the last decade, it might be safe to assume that as the Windows CE operating system is enhanced over time, more diverse applications will be developed to solve problems we experience today. Some solutions might stretch the Windows CE architecture to its limits, performing tasks that we would not have believed possible. At the moment, Microsoft envisions that Windows CE will be best suited to the three main categories of product shown in Figure 5-1.

Microsoft has divided its support for the application of Windows CE into two areas: semi-targeted products and targeted products. Essentially, for semi-targeted products, Microsoft will work with OEMs and IHVs to produce custom components for a specific purpose (for example, writing device drivers). For targeted products, Microsoft will make enhancements to the operating system itself to support features required by certain types of application. One example of a targeted product is the HPC, where the operating system has built-in support for most of the features it requires.

Click to view at full size.

Figure 5-1 The main Windows CE product categories and typical usage

Building a Windows CE Device

Hardware vendors are generally very good at what they do, maybe because of the skills they need to produce a piece of equipment that meets the tough demands of consumer laws. When, for example, was the last time you bought a car with a bug in the electronic ignition system? OK, so you might not know if the car did have one, but compare that to the average software application! With this degree of technical ability, the average IHV or OEM will easily be able to build high-quality devices using Windows CE.

Windows CE is purchased as a set of files containing the operating system binaries—the exact content is decided by the purchaser. The Windows CE Kernel module is a mandatory requirement of the operating system; however, all other components are optional. For example, if you were building a control module for an alarm system, you might want to have a custom LCD display, in which case you would purchase Windows CE without the Graphics Device Interface (GDI) module and simply add your own. You get the Microsoft Windows CE Embedded Toolkit for Visual C++ 5 (ETK) along with the operating system. This toolkit allows you to build device drivers that interface your hardware to Windows CE and customize the operating system. (See Figure 5-2.)

Click to view at full size.

Figure 5-2 Windows CE is available in various configurations to match the user's needs

In the early days of PC software development, a major consideration was that of processor speed and disk space. As hardware has evolved, the cost of high-speed CPUs, RAM, and disk storage has plummeted, in many cases reducing the need for programmers to spend time developing code that conserves these resources—the average corporate desktop PC will usually have adequate memory and disk space to run the application. Windows makes things even easier with its virtual memory management. However, the story is very different for OEM and IHV developers. In a custom device, physical space might well be at a premium. More importantly, hardware like CPUs, ROM, RAM, and disks all consume power, which is limited in a portable device. This makes the manufacturer's task a difficult one. For example, a color display might be a nice thing to have, but you might have to cut back elsewhere so that the poor battery will last a reasonable time. (Remember the first portable phones with their enormous battery packs?) This is the reason that the Windows CE operating system is vastly reduced in size from the PC and server operating systems, and it is also why the operating system is available in component form. By including only the required functionality, the manufacturer can keep the ROM requirement small, which allows more RAM for the user.

Having purchased the Windows CE license, you now want to build and test your device. You can select a Windows CE configuration with as many or as few components as you require. For example, if you want telephony support you might choose to purchase a license that includes the TAPI module. The more components you choose, the easier it will be to build your device because much of the work will already have been done. After selecting the operating system components, the vendor must build any drivers that will interface to the device hardware. In addition, the vendor must write any vendor-specific software. For example, not all devices need to have a video display unit (VDU)/keyboard interface; a Windows CE stereo system might have a custom plasma display with a button panel. In instances like this, the vendor might need to write his or her own components—in this case a replacement GDI module and hardware drivers. The Windows CE Embedded Toolkit for Visual C++ 5 provides all the necessary support for building and testing device drivers and customizing the operating system. Once the software is written, the next step is to burn an erasable programmable read-only memory (EPROM) chip for testing. The EPROM contains the user-configured Windows CE operating system in addition to the device's software. Having the software in ROM does not prevent additional software from being loaded into RAM, but RAM software is lost should power fail. However, an advantage to having software in RAM is that it makes upgrading a much easier task. Once the device has been tested and debugged, the "real" system CPUs can be produced for the finished device. For an OEM or IHV, producing a Windows CE device should be reasonably straightforward; in many cases, it should be an easier task than at present once the OEM or IHV has mastered the intricacies of the Win32 API and Windows CE configuration.

It is easy to draw false impressions about the capabilities of the Windows CE operating system, especially if you focus too much on devices like the HPC. Consider the screen display as a prime example. When I first started to look at Windows CE seriously, I thought the operating system was bound to a miniature device that had to have a tiny screen display. In fact, nothing could be further from the truth—the Windows CE device can be of any size and can also incorporate a screen display of virtually any size.

Peripheral devices, of which I predict a weird and wonderful selection, will be designed primarily for a specific task. Windows CE allows flexibility for this specific design; for example, the operating system can support a screen resolution of 1600 x 1200 and device drivers can be built if the default does not meet a particular requirement. If I could emphasize one Windows CE concept, it is that Windows CE is a flexible and adaptable compact operating system.

Windows CE has matured somewhat over the last year or so and, as with any good product, a whole plethora of support tools and services are now available to both the OEM and software developer.


Getting Under the Hood

Many programmers are now familiar with the Windows architecture and have some knowledge of hardware used on platforms like the Intel x86. It would be wise to understand the principles of a platform before writing an application for that platform. For this reason, this section provides a brief overview of the core components that make up Windows CE. Please bear in mind that this section is not designed to be an exhaustive reference.

Supported Architectures

Microsoft's desktop and server operating systems presently support a limited number of platforms, such as Intel x86, Alpha, MIPS, and so forth. However, to target the mass electronics market, Windows CE must provide support for a vastly larger number of processors. After all, it is unlikely that a vendor using a tried and tested processor will want to change to an unfamiliar environment that might require a whole new learning curve and changes to test-bed equipment. Microsoft's commitment to Windows CE is such that even at this relatively early stage, support is already provided for CPUs from eleven manufacturers, and the list is growing! Currently support is provided for processors from the following CPU manufacturers:

  • AMD

  • Digital

  • Hitachi

  • IBM

  • Intel

  • LG Semiconductors

  • Motorola

  • NEC

  • Philips

  • Toshiba

  • QED

At present, the Microsoft Windows CE Toolkit for Visual C++ 5 can create programs for each of these platforms; however, the Microsoft Windows CE Toolkit for Visual Basic 5 can only create applications for HPC devices. At present, Philips and Hewlett-Packard are the two largest players in the commercial HPC market, the former using the MIPS platform and the latter using the SH3 platform from Hitachi. This supported hardware list will increase over time, I imagine, according to the demand from customers. I would also expect that the platforms available for Visual Basic will increase. This degree of flexibility is one reason why the industry is taking Windows CE very seriously.

Win32 API

Microsoft estimates that there are some 4.76 million professional developers worldwide, a large number of which currently program Win32 using languages such as Visual C++ or Visual Basic. Of this number, it is estimated that around 300,000 are embedded developers (developers who write software to control hardware devices). For this reason, basing Windows CE on the Win32 API provides a sound foundation from which to build. Don't be fooled, though; if there were not such a large user base, it is feasible that Windows CE might have been based on some other API. With a development team of around 700 on Windows CE alone, Microsoft is more than capable of achieving this!

The API set for Windows CE is very much scaled down from the desktop and server operating systems. Whereas Windows NT has around 10,000 API routines, Windows CE has a mere 1200. This isn't a bad thing, because the new versions are highly optimized and the duplicated functions that exist in the Windows NT/9x operating systems have been removed. If you will be porting from an existing application to Windows CE and you use Win32 API calls in your code, I hope you will have converted to the newer calls. For example, where a function Foo and FooEx exists you should be using the newer FooEx version. If you haven't then never mind, the conversion should not be too painful, although you will need to convert because the older routines do not exist in Windows CE.

The subset of API calls should be sufficient for most tasks. Remember that Windows CE has been built with a "ground-up" approach, so the immediate requirements have been dealt with first. As the operating system's development progresses, new calls will be added as required by users.

The Object Store

The object store is the collective name for the data storage elements within the Windows CE operating system. On the HPC, physical RAM is divided into application memory space and emulated disk space. The disk portion of RAM is where the object store resides. The object store is home for such items as the Registry, Windows CE databases, user files, and applications. Applications that are built into the operating system core are actually held in ROM and cannot be accessed using the object store API calls. A default Registry is also stored in ROM and therefore it, too, cannot be accessed using the API.

An important feature within the Windows CE object store is its transaction mechanism. Microsoft stipulates that if for any reason data cannot be written to the object store, the whole operation will be canceled. This mechanism works on a record level—for example, if you are writing 10 records to a database and a power loss occurs during record 8, records 1 to 7 will have been saved, but no fields in record 8 will be. This applies to the Registry and file system in the same way.

The object store comprises three elements: the database, the Registry, and the file system. These are explained in the following sections.

The Windows CE database

The Windows CE database model provides storage for non-relational data. You can create any number of databases, each with a unique ID. Each database can contain any number of records (depending on memory). A database record can have any number of fields and these fields can be of type Integer, String, Date or Byte array (also known as Binary Large Objects, or BLOBs). Each database record has its own unique ID and this ID is unique across all databases. Database records can contain up to four index keys, which can be used for searching and sorting. In addition, a field can be indexed either in ascending or descending order.

The records held in a database need not be related; for example, you can store contact information in some records and product price details in another record within the same database. A number of API functions exist to manipulate databases—these are covered in more detail later in this chapter. Microsoft has recently released a beta version of ActiveX Data Objects, (ADO) which provides full connectivity to relational databases like Microsoft SQL Server and Microsoft Access. ADO makes it possible to manipulate data in a relational database using the standard SQL to which most Windows developers are accustomed.

Even though the standard database features might sound rather limited, you should remember the kind of application for which Windows CE is designed. If you think in terms of the OEM developing a household device, you'll see that the functionality is more than adequate.

File system

The Windows CE file system has a host of functions with which to access file-based data. The file functions that exist in the normal development environment are not supported by Windows CE; instead, new functions have been provided in the API. Most Windows CE devices presently use the FAT-based file systems. However, the operating system can support installable file systems. For most current Windows CE applications, file access will be to RAM rather than to a physical hard disk, although in terms of coding the difference is transparent.

The file system can be browsed and manipulated using the Mobile Devices application that is installed as part of the Windows CE Services. This application is rather like Windows Explorer and works in the same way. You can even copy, move, or paste files between Windows Explorer and mobile devices.

Registry

Windows CE, like Windows NT/9x, uses the Registry to store system and application configuration data. However, the Windows NT Registry consists of several files, or hives, that together form the entire Registry. Windows CE does not support hives—the entire Registry is stored as a single file. The Registry file is stored in ROM and is copied to RAM when the device is booted. You should bear in mind that a Windows CE device will probably boot only after the power supply has been connected. Normally, when the device is turned off, backup batteries retain RAM memory. This design allows a feature that Microsoft calls "Instant On"—that is, the device is immediately available when switched on. It is possible to write software that saves the Registry file to a non-volatile storage device. Given the nature of the power supply for many prospective Windows CE devices, however, user data can be lost in almost any situation. Losing the Registry should not cause too many problems for an application, because more than likely any user files will be lost as well. A good design principle to employ might be for an application to back up the Registry to non-volatile storage whenever the user chooses to backup his or her files.

The RAM copy of the Registry can be accessed using the Win32 API, or Visual Basic programmers can use the built-in Registry functions. Desktop applications written in Visual Basic often need to use the API in order to access different Registry keys; for example, global application data would probably need to be saved to HKEY_LOCAL_MACHINE, whereas user specific settings would be better located under HKEY_CURRENT_USER. For Visual Basic programmers, a COM DLL is required to access keys or paths other than the default one that Visual Basic accesses: HKEY_CURRENT_USER\Software\VB and VBA Program Settings.

ActiveSync

ActiveSync is the technology introduced with Windows CE 2 that provides an easy way to keep data synchronized between a mobile device and a desktop PC. ActiveSync allows you to keep databases synchronized between your device and the desktop PC in much the same way as replication occurs between SQL Server databases. Conflict resolution is handled for you once you set up the required configuration. The synchronization operations can be performed using the Mobile Devices folder menu options, but you can also use certain API functions to control the process. The ActiveSync API calls are listed later in this chapter in the section "Visual Basic Development".

Processes and Threads

Because Windows CE is based on the Win32 API, it has full support for processes and threads. Visual C++ (and even Visual Basic 5) programmers might already be familiar with these concepts. Essentially, a process consists of one or more threads of execution, where a thread is the smallest unit of execution. Windows CE allows a maximum of 32 processes; however, the number of threads is limited only by available memory. It is possible to create threads using Visual Basic, but this is not advisable because the Visual Basic run time is not "thread safe"— that is, if two threads within the same process were to call a Visual Basic routine at the same time, some fairly nasty things might happen. However, because I know that some of you will try it, and because it is very applicable to C++ development, the section "Visual Basic Development" later in this chapter describes the thread-handling API calls. If you have trouble remembering the difference between processes and threads, you might find the diagram in Figure 5-3 helpful.

Click to view at full size.

Figure 5-3 Windows CE processes and threads

The ability to assign priorities to a thread is a major requirement, especially for an operating system that will host real-time applications. Windows CE allows a thread to be assigned one of eight priority levels, as shown in Table 5-1.

Table 5-1 Thread Priorities

Priority | Typical Usage
0 THREAD_PRIORITY_TIME_CRITICAL | Used primarily for real-time threads and processing, such as device drivers. Priority 0 threads are not preempted—once started, a thread process will continue to completion. The operating system will not interrupt the thread.
1 THREAD_PRIORITY_HIGHEST |
2 THREAD_PRIORITY_ABOVE_NORMAL | Kernel threads normally run at these levels, as do normal applications.
3 THREAD_PRIORITY_NORMAL |
4 THREAD_PRIORITY_BELOW_NORMAL |
5 THREAD_PRIORITY_LOWEST | Used in instances in which it doesn't matter how long the functionality takes to complete. These will usually be background tasks, probably without their own user interface. An example might be of a thread that periodically checks to see if you have any new mail. Threads on these priority levels can expect to be interrupted often.
6 THREAD_PRIORITY_ABOVE_IDLE |
7 THREAD_PRIORITY_IDLE |

The Windows CE operating system is preemptive and as such must allocate a time slice for each thread. This is done in a "round-robin" fashion. The default time slice for a thread is 25 milliseconds, except for priority 0 threads. A priority 0 thread, once started, will retain the processor until it yields control. The scheduling mechanism uses an algorithm whereby the highest priority thread is always allocated time first. This process is better illustrated by Figure 5-4.

Click to view at full size.

Figure 5-4 Thread preemption in Windows CE

Windows CE handles thread priority inheritance (a requirement of real-time systems we will discuss shortly) using a method called Priority Inversion. It is possible that a thread on a lower priority might lock a resource required by a thread on a higher priority. When this condition occurs Windows CE promotes the lower thread task to the level of the higher priority thread until the resource has been released. The Win32 API has full support for thread priority assignments. In version 2 of the Windows CE operating system the default time slice is configurable on the MIPS platform.

An additional requirement currently being developed is to increase the number of priority bands, possibly to as many as 256. This has been a frequent request from OEMs and IHVs in order to enhance real-time flexibility. Because of the way in which the preemptive multitasking works in Windows CE, it is possible to guarantee the time it will take for a thread to execute on the highest priority, an important factor in a real-time system.

Real-Time Capabilities

The ability for Windows CE to perform real-time processing is an essential element when it comes to control and monitoring systems. There is some debate as to whether the operating system currently allows "true" real-time processing. The Internet newsgroup comp.realtime gives this standard definition of a real-time system: "A real-time system is one in which the correctness of the computations not only depends on the logical correctness of the computation, but also on the time at which the result is produced. If the timing constraints of the system are not met, system failure is said to have occurred." In addition, this newsgroup states that a real-time operating system must also meet the following requirements:

  • The operating system must be multithreaded and preemptive.

  • The operating system must support thread priority.

  • A system of priority inheritance must exist.

  • The operating system must support predictable thread synchronization mechanisms.

  • The maximum time during which an interrupt can be disabled by the operating system or device drivers must be known.

  • The time it takes the interrupt to run (interrupt latency) must be within the requirements of the application.

The Windows CE operating system meets the criteria to be classed as a real-time operating system, but in its current implementation the architecture does not achieve these goals in a way that would promote the level of integrity required to host a mission critical application. To help you understand this better I should perhaps explain how the interrupt processing is performed. Interrupts are events triggered by external components to announce that an event has occurred. Because I know a little about aircraft, I shall use an aircraft warning system to draw an analogy—note that the examples are not strictly accurate but are simply designed to illustrate the point. Imagine an autopilot that is equipped with a collision avoidance system. In the event of a collision warning, the collision avoidance device should notify the autopilot, which in turn must take corrective action. This might be in the form of an audible warning and also might automatically adjust the aircraft controls to miss the obstacle. In a real-time system you would expect that once the warning (or interrupt) has occurred the autopilot will react and perform the required actions within a stipulated time. Figure 5-5 shows a simplified diagram of how such functionality might be handled by the Windows CE operating system.

Click to view at full size.

Figure 5-5 Real-time processing in Windows CE

An external device notifies the operating system of an event by way of an interrupt, or more correctly, an interrupt request line (IRQ). Each IRQ has an associated interrupt service routine (ISR). When an interrupt occurs the Kernel calls the associated ISR, which returns the ID of the interrupt service thread (IST) for the IRQ. Once the Kernel has the thread ID it notifies the IST, which starts the required processing. There are a couple of reasons why you might not want this system in a Boeing 777 on final approach. First, interrupts cannot be nested—that is, once an interrupt has occurred no further interrupts can be processed until the IST has started for the previous interrupt. Second, imagine the scenario where there are multiple ISTs each on 0, the highest priority. Because critical threads are not preempted any further IST will not be able to run until the first IST has finished. So, in the case of our 777, whose computer also handles the fire extinguisher, we could deal with the collision warning, but we could not deal with a fire until the collision warning's IST completed. Microsoft is working hard to cater to these demanding requirements of real-time mission critical applications. Version 3 of the Windows CE operating system will be able to handle nested interrupts and will have interrupt latencies of less than 50 milliseconds.

Development Environments

One of the attractions of the Windows CE operating system is that it is possible to build software applications using industry-standard programming languages. For the OEM and IHV developers, the choice of programming language will no doubt be the ETK because of their need to write low-level device drivers. For the rest of the industry, Windows CE also provides support for C++, MFC, ActiveX Template Library (ATL), Visual Basic, and Java applications. The Visual Basic language might prove very appealing because it is so well known, but note that you cannot use the Windows CE Toolkit for Visual Basic with any version other than Visual Basic 5 at the moment—the toolkit is hard-coded to recognize only this version. Other higher-level applications, especially Web-enabled ones, can be written using Visual J++.

Whichever tool you use to write Windows CE applications, you must bear in mind that they are subsets of their respective "parents." When we discuss Visual Basic development later in this chapter, you will notice a marked reduction in the number of routines and statements available. Obviously, the scaled-down nature of the operating system means that certain functionality is not needed because Windows CE cannot support it. The other reason for some omissions is that Microsoft has included what they believe to be the most needed features for this platform—don't worry, the feature list will grow with demand. Currently four main markets exist for Windows CE (although this is growing). These are:

  • Auto PC (in-vehicle computing)

  • Handheld PC

  • Palm-sized PC

  • Embedded systems

The language you choose depends largely on which of these platforms you will be developing for. Figure 5-6 shows the various development tools available for each platform. The long-term strategy, as far as development tools is concerned, is for each toolkit to provide support for each platform. As the tools are more finely honed and expanded, the software development scene will change somewhat, presenting exciting opportunities. I personally look forward to the possibility of projects including mission-critical systems and real-time systems in the future.

Click to view at full size.

Figure 5-6 Choosing the right development tool for the job


Windows CE and the IT Department

The development of applications for Windows CE falls mainly into two camps: embedded systems for custom hardware devices and high level applications for devices such as the HPC and the palm-sized PC. I would imagine that the development of software will be split into low-level development for the OEM and IHV and high-level development for the corporate Information Technology (IT) environments. Although some companies might use custom Windows CE devices, it is probable that they will use ready-built hardware and write their own custom high-level applications.

I imagine that many corporate developers might at this moment have visions of writing sales force automation systems and so on, but they (and everybody else) must consider their hardware limitations before deciding how best the technology can be utilized in their particular environment. For example, it might not be a good idea trying to port a full-blown ordering system to an HPC because of the memory constraints. The power requirements of HPC_style devices will continue to be a hindrance that prevents mass storage capabilities, although even as I write there is a company developing a fingernail-sized hard disk capable of storing a half megabyte of data. Battery technology will improve over time, but in the past advances in this area have been nowhere near the advances in hardware technology.

For the more conventional software houses and IT departments a whole new market will open up in areas such as point-of-sale systems, bar code information retrieval, and other data capture devices. The cost benefits are numerous; for instance, businesses such as local electricity companies or traffic enforcement agencies could invest in HPC machines for their meter readers to collect data off site, and then use the ActiveSync or ADO technology to upload data to the corporate database. The cost of an HPC is probably considerably less than the cost of custom hardware devices, and, as we have discussed, writing programs for these devices is a pretty easy task.

For the time being, I do not expect to be writing any aircraft control systems. However, with the enhancements being made to the real-time capabilities of Windows CE, it is quite possible that specialist companies might open their doors to the contract market or independent software houses. Basically, the future is not set—what we are seeing in Windows CE is a way forward that opens up many new niche areas.


Visual Basic Development

To develop Windows CE applications using Visual Basic, you will need the Windows CE Toolkit for Visual Basic 5. Notice that at the moment the toolkit will not run with any other version of Visual Basic. The toolkit provides the Windows CE-specific functionality and the IDE changes needed to create and build Windows CE applications. In terms of the language, Visual Basic for Windows CE is a subset of Visual Basic, Scripting Edition. This means that much of the Visual Basic 5 language is not supported. However, enhancements have been added to the language in Visual Basic 6, but not Visual Basic 5. This chapter is aimed at developers who are already experienced in Visual Basic 5 development and, therefore, this section focuses mainly on the differences and new features of the language and environment.

The Development Environment

Creating a new Windows CE project is not much different from creating a normal Visual Basic one. A new project type, Windows CE Project, has been added, which configures the Visual Basic IDE for Windows CE development. In standard Visual Basic you can create a number of different types of project, such as Standard EXE, ActiveX EXE, and ActiveX DLL. However, a Windows CE project might only create the equivalent of a Standard EXE, or—to be more precise—a PVB file. Before you commence coding, you must configure your project's properties. The Project Properties dialog box is displayed automatically when you start a new project. Once you have dismissed the Project Properties dialog box, you will notice some changes in the IDE from that of standard Visual Basic. Figure 5-7 shows the major changes to the IDE.

Figure 5-7 Windows CE IDE changes in Visual Basic 5

The first things you will notice are the greatly reduced number of options for the Run and Debug menus. This is because the way that Windows CE programs are run in the development environment is very different from the way a standard Visual Basic project runs. The toolkit provides an emulation environment that allows you to run and debug your applications without actually having an HPC device. I will explain the emulator in more detail later, but essentially, the emulator is part of the Windows CE Platform SDK and is supplied with the Windows CE Toolkit for Visual Basic 5.

A number of new menu options have been added to help with Windows CE development, as listed here.

  • The Application Install Wizard, as the name suggests, provides the equivalent functionality as the Visual Basic Setup Wizard.

  • Books Online contains reference information and is very comprehensive. Additional information can be obtained from the Microsoft Web site.

  • Download Runtime Files transfers the Visual Basic run time files to the emulation and HPC device.

  • Control Manager downloads ActiveX controls to either the emulation or HPC environment. Any controls you use in your application will need to reside in the environment where you choose to run or debug the application.

  • Heap Walker (a scaled-down equivalent of the program supplied for other Windows versions) views the process and "heaps" information for processes running on your HPC.

  • Process Viewer provides the functionality of the PVIEWxx.EXE program supplied with other versions of Visual Basic. Process Viewer lists each module loaded in memory on the HPC. You can use this application to kill processes running on your HPC and you can also view the modules being used by a particular process.

  • The Registry Editor functions the same way as in the other Windows operating systems. This version, however, allows you to edit both the HPC and emulator Registries.

  • The Spy utility allows you to examine details of window objects that are loaded on the HPC. Like the Windows NT/9x version, Spy allows you to view the window and class information and to view the message queue to a particular window on the HPC device.

  • Zoom was originally designed to allow you to zoom in on an area of the screen to view the bit patterns. The Windows CE version has extended this functionality to allow you to also save screen shots of your HPC screen.

Windows CE Application Design Philosophy

Because of the nature of the Windows CE operating system, a new design philosophy is required in order to develop Visual Basic applications for the HPC. The foremost concern is that of memory. With such a potentially small amount of RAM you stand a good chance that your application might run out of memory or be terminated when another application requires memory resources being used by your application. Unlike Windows NT/9x, there is no virtual memory management, so if memory runs out—tough! Once the machine starts running low on memory it will look to see if there are any other applications running. If another application is found, it will be terminated; this will continue until the memory requirement has been satisfied. In effect, any program in memory is in danger of being shut down by the operating system to satisfy memory requirements. A Windows CE application must be designed with this in mind. User transactions must be well designed so that in the event of an application being closed, user data is not left in an unpredictable state. The operating system's transaction mechanism will protect you against data integrity problems at a record level, but you can build certain scenarios into your code that increase the risk of data getting out of sync. For example, if you are writing a series of related records from various sources, it might be a good idea to collect all the data and then apply the changes in a batch, rather than as each individual item becomes available.

Another consideration is that of the power supply. The nature and size of HPC devices means that at some point the battery will run out. HPC devices usually contain backup batteries that are designed to preserve the memory while the main batteries are being changed. However, the backup batteries can be removed as well. An application must allow for the possibility of power loss. In this instance, batch operations will not be of any use. When power is lost, the entire content of the RAM is lost. The only safeguard against losing data is to back up to a PC. Do not confuse loss of power with the user switching off the device. In the latter case, RAM is not destroyed—the HPC merely enters a sleep state. When the HPC is turned on again, the device's previous state will be restored. As a Visual Basic developer, you will very likely be writing high level applications and as such, if there is a loss of power, the application will no longer be in memory. Unless the user's data has been synchronized (saved to a desktop), the data will also be lost.

In terms of visual appearance, the potentially small screen display might mean cutting back on some of the more aesthetic user interface elements—a good practice would be to make sure the elements are functional rather than cosmetic. In most cases I would advise against using unnecessary controls in your application because each control you include in your project will need to be installed on the device. This will leave less working memory. You should also give consideration to the ergonomics of the interface; for example, if you are not using default color schemes, you should make sure that the contrast is sufficient for both gray-scale and color displays.

Your First Windows CE Application

Writing a Windows CE program is much the same as writing any other Visual Basic program. However, you'll have to accustom yourself to many differences in the language and to the development environment. You'll need to carefully consider the structure of your code and the implementation of the finished application. I have written a card game named Pontoon, and I will use this application from time to time to highlight important points. Figure 5-8 shows some screen shots of the application.

Perhaps now might be a good time to explain quickly the rules of the game—it's similar to Blackjack, or 21. The player plays against the computer (dealer) with the aim of attaining a score of 21. If the score goes over 21, the game is lost. The player can choose to be dealt another card, ("twist"), and his or her score is calculated as a sum of the face values of all the player's cards. The ace has a face value of 11 and picture cards have a face value of 10. If the player decides that another card might bring his or her score over 21, he or she might decide to not take anymore cards ("stick"). After a player chooses to stick, all subsequent cards will be dealt to the dealer. If the dealer reaches a score higher than the player's and below 22, the dealer wins. But if the dealer's score exceeds 21, the dealer loses. Before the player makes the first twist he or she must select an amount to gamble. If the game is lost, the player's funds are deducted by that amount. If the player wins, his or her funds are increased by the amount gambled. That's it! The full source code for the Pontoon game is included on the CD-ROM accompanying this book.

Figure 5-8 The Pontoon Windows CE card game in action—I got lucky this time!

General Design Considerations

Building a Windows CE application is essentially a compromise of various factors, more so in Visual Basic because of the small subset of available programming constructs. The common factors that you will need to consider are size, memory, maintainability, and stability. All these factors together determine the quality of the application as shown in Figure 5-9.

Figure 5-9 Design factors in a Windows CE application

For my Pontoon game, the primary goals are good maintainability and small size. Various techniques are used to reduce the program size while trying to keep it reasonably understandable and maintainable; however, this may result in a loss in performance and perhaps in robustness. But the losses are not so great because stability is not a major concern. We can code to avoid obvious errors, but if an error does occur, at worst the user will get an error message. Speed is not really important here because the actual processing cycles are relatively small.

The user interface

The most obvious consideration when writing an HPC application is that of the screen display. The current HPC machines have a relatively small screen resolution, though these do vary between models. Apart from the physical dimensions, color is also an issue. Version 2 of Windows CE introduced support for 16 colors. This has improved the color contrast on monochrome displays, though devices are now available with color displays.

Windows CE supports only two window styles—fixed border or no border—and there is no Minimize or Maximize button. You can set the form's BorderStyle design time property to another style but any styles other than those allowed will be overridden at run time. When creating a new form, Visual Basic defaults to a size near the maximum resolution of the HPC device you are using, but you can change this default size in the Project Properties dialog box. Any size that you set here will be retained as the default for future forms you create.

A new window style has been implemented for message box windows. If you specify a button configuration of vbOKOnly, the message box will be displayed with the OK button in the title bar next to the Close button. Other button configurations will display message boxes in the usual format. The height of the message box has been scaled down to obscure the least amount of space on the screen. While we're on the subject of message boxes, you should be aware of a glitch in the current version of the language. The message box is not modal, but floats above its parent window. The form below the parent window can still respond to mouse events. To avoid this problem you will need to disable your form while the message box is displayed.

When designing a Windows CE form, you should evaluate the need for a border and title bar. Many of the Microsoft applications, such as Pocket Word and Pocket Excel, do not have title bars or borders. This really does increase the amount of usable screen real estate.

In terms of user interface controls, you will find that not all of the intrinsic Visual Basic controls are supported. Rather, the intrinsic user interface controls you can use are:

  • Check Box

  • Combo Box

  • Command Button

  • Frame

  • Horizontal and Vertical Scroll Bars

  • Label

  • Line

  • List Box

  • Option Button

  • Shape

  • Text Box

  • Timer

Although the remaining controls are not removed from the toolbox, a warning message will be displayed and the control will be removed if you attempt to place one of these on your form. Obviously, graphical capabilities must be retained, so two new graphical controls are available from the Components dialog box: PictureBox and ImageCtl.

These two graphical controls are replacements for the standard PictureBox and Image controls, though you should note that their class names have changed. These two controls retain the ability to display images and pictures, although there are some differences from the controls they replace. Apart from being a lightweight control, the PictureBox has undergone some changes to its methods. The methods such as Line, Circle, Point, and PSet have been removed and are now replaced by this new set of methods:

  • DrawCircle

  • DrawLine

  • DrawPicture

  • DrawPoint

  • DrawText

Pontoon, being a card game, relies heavily on graphics. The graphical methods supported by the Form object in other versions of Visual Basic are not available in the Windows CE toolkit. Therefore, the PictureBox control is used for displaying the graphics.

Windows CE contains a unique set of constraints or bounds that we must work within. One such constraint is that control arrays are not permitted. You can create control arrays but you will get an error at run time if you do so. In the case of the Pontoon game, the work-around to this problem is to use a PictureBox control and draw the card graphic in the picture box. In other versions of Visual Basic, it might have been easier to simply create a control array of Image controls on the fly, and then load the required images. The Pontoon game uses the Windows CE PictureBox control as a container into which the cards can be drawn. The PictureBox control does not have the ability to be a container object, so a problem arises because you cannot place labels within the PictureBox. Because labels are lower in the z-order, you can't show labels within a picture box. To get around this problem I've used two picture boxes to display the rows of cards and I've used a Shape control to create an area around the picture boxes and labels to form a playing table.

Figure 5-10 shows the Pontoon screen at design time. The number of controls have been kept to a minimum. The screen is built up using only essential controls or elements needed to improve clarity.

Figure 5-10 The design-time layout of the Pontoon game

You will notice that although the design-time form looks like a standard Windows NT/9x form, the form will be displayed using the Windows CE window style when the program is run—that is, with no Minimize or Maximize buttons. We have an interface where all elements are large enough to be clearly visible, resulting in little clutter on the screen.

Another important aspect of the user interface design is that of keyboard ergonomics. If I were sitting on a train playing this game, I might find it uncomfortable trying to use the stylus if the train is swaying. It might also be uncomfortable to use the accelerator keys because two hands are required. One design feature I've implemented to aid keyboard use is the KeyPreview property, which is used to intercept keystrokes and convert them to accelerator key presses. In an ideal world we could simply code the following in the form's KeyPress event, as shown here:

Private Sub Form_KeyPress(KeyAscii)
    SendKeys "%" & Chr(KeyAscii)
End Sub

Alas, this is not possible—the SendKeys statement is not supported. Instead you can achieve the coding using a method like the one I've implemented here:

Private Sub Form_KeyPress(KeyAscii)

    If StrComp(Chr(KeyAscii), "g", vbTextCompare) = 0 Then
        txtGambleAmount.SetFocus
    End If

    If StrComp(Chr(KeyAscii), "t", vbTextCompare) = 0 Then
        cmdTwist.Value = True
    End If

    If StrComp(Chr(KeyAscii), "s", vbTextCompare) = 0 Then
        cmdStick.Value = True
    End If

End Sub

Simple routines like this take no time to write but can drastically improve the ergonomics of an interface. I expect many new ideas to be developed in the future that will aid usability.

In addition to the intrinsic controls and the PictureBox and ImageCtl controls that are supplied with the development kit, you can also download a set of controls from the Microsoft Web site at http://www.microsoft.com/windowsce/developer. At this site you can obtain the Microsoft Windows CE ActiveX Control Pack 1.0, which contains many more useful controls. The download is more than 5 MB but is worth downloading, because in this pack you get the following controls:

  • GridCtrl

  • TabStrip

  • TreeViewCtrl

  • ListViewCtrl

  • ImageList

  • CommonDialog

The addition of these controls allows you to create the same interface styles as the controls of full-blown Visual Basic applications. The list of controls will grow and I would expect a lot of new controls to emerge from third-party vendors as well.

Size and memory considerations

I said earlier that one of the goals for the Pontoon game was to be small in size. The word "size" might imply the physical file size, but an important factor is also the memory footprint of the application. You can control the program's memory footprint by enforcing restrictions on functionality and by writing more efficient code. The former method will nearly always be a business issue and, therefore, possibly might be out of the programmer's control. However, using efficient coding techniques is a trade-off against readability and maintainability. However, I'll discuss some techniques that you can use to code more efficiently.

Program variables are the obvious source of memory consumption. The Windows CE Toolkit for Visual Basic 5 allows only Variant type variables. This is a little surprising, given that Variants take more memory than a "typed" variable. Although your variables will be Variant types, you can still coerce them into a subtype using the conversion functions like CLng, CCur, and so forth, although this coercion will be performed automatically when a value is assigned to the variable. The Pontoon game makes extensive use of bit flag variables. This is an efficient way to store multiple values, providing there is no overlap in range of the bit values. By using bit values, the overall memory requirement can be reduced, but you must be careful if creating constants to represent the bits because you might end up using the same or larger amounts of memory. The following is the declaration section from the form:

Private m_btaDeck(12, 3)

Private m_btaPlayerCards        ' Byte Array stores cards held by player.
Private m_btaDealerCards        ' Byte Array stores cards held by dealer.
Private m_nPlayerScore          ' Player score - total value of cards held.
Private m_nDealerScore          ' Dealer score - total value of cards held.
Private m_nPlayerFunds          ' Dealer score - total value of cards held.
Private m_nGameStatusFlags      ' Long Integer of flags indicating game
                                ' status.

' Constants used with m_nGameStatusFlags.
'
Private Const m_GSF_PLAYER_HAS_21 = &H1
Private Const m_GSF_DEALER_HAS_21 = &H2
Private Const m_GSF_PLAYER_HAS_BUST = &H4
Private Const m_GSF_DEALER_HAS_BUST = &H8
Private Const m_GSF_PLAYER_HAS_HIGHER_SCORE = &H10
Private Const m_GSF_DEALER_HAS_HIGHER_SCORE = &H20
Private Const m_GSF_PLAYER_HAS_STUCK = &H40
Private Const m_GSF_IS_DEALER_TURN = &H100

You should note two points here. First, even though you can declare only Variant variables, it is still good practice to use scope and type prefixes. Because each variable can potentially store any value, you need to be sure that everyone knows what type of value is expected. For example, the variable UserName obviously contains a string, but a variable named ItemId could easily represent a string or numeric value. Second, you'll see that I've used hexadecimal notation to assign the bit flag constants. Bit manipulation often requires mask and other values to extrapolate the individual values within the flag variable. Using hexadecimal notation makes it much easier for others to understand the operations that are being performed, because the conversion from hexadecimal to binary is much easier than from decimal to binary.

Let's look for a moment at the variable m_nGameStatusFlags that I use in the Pontoon game to keep track of the game's progress. The variable is a Long Integer (32 bits) and stores nine separate values, which together provide a complete picture of the game's current status. Figure 5-11 shows how these values are stored.

Figure 5-11 Pontoon game bit flag values

Another technique you can use to reduce memory size is to pass all parameters by reference (ByRef). Doing this means that a pointer to the variable is passed and the procedure does not need to make a local copy of the variable within the procedure it is being passed to. In other versions of Visual Basic, passing by reference would not be good practice because of the potential of inadvertently changing a parameter's value, affecting code outside of a procedure. Many of the Pontoon game's functions are designed to process data that is held in module-scoped variables. However, it is still a good idea to pass the module-level variable into procedures, because this improves reuse as the procedure does not get tied to the module or form where the variable is declared.

A common problem when trying to optimize code is that complex algorithms are often created, which can be very difficult to understand. It is a good idea to encapsulate complex functionality within distinct functions so that if maintenance is required, the functionality is not shrouded by unnecessary code. An example of this encapsulation is the function below that shuffles the deck of cards in our Pontoon game.

Private Sub ShufflePack(btaDeck, btaHand1, btaHand2, nGameStatus)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Shuffle the pack. We acheive this by marking each byte in the card '
' deck array as being available (False). Obviously we cannot unmark  '
' any card that is currently held by the player or dealer.           '
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Dim bteCard         ' Value of card being processed.
    Dim nCard           ' Counter for iterating our card values.
    Dim nSuit           ' Counter for iterating suit values.

    ' Mark each card in our array as False, meaning available.
    For nCard = LBound(btaDeck, 1) To UBound(btaDeck, 1)
        For nSuit = LBound(btaDeck, 2) To UBound(btaDeck, 2)
            btaDeck(nCard, nSuit) = False
        Next
    Next

    ' Loop through the player's cards. Each of the player's cards
    ' must be made unavailable in the deck by marking it True.
    If IsEmpty(btaHand1) = False Then
        For Each bteCard In btaHand1
            ' Calculate the array index for the card and set
            ' its element to True, indicating the card is in
            ' use. Bit 9 of the card array is the suit and bit
            ' 1-8 is the card's value.
            btaDeck(bteCard And &HF, (bteCard And &H70) \ &H10) = True
        Next
    End If

    ' Do the same for the dealer's cards.
    If IsEmpty(btaHand2) = False Then
        For Each bteCard In btaHand2
            btaDeck(bteCard And &HF, (bteCard And &H70) \ &H10) = True
        Next
    End If

    nGameStatus = (nGameStatus And &HFF)

End Sub

Looking at the code above you can clearly see all the actions for shuffling the deck. Such a procedure might be called only from one location in your program, but placing the shuffling code in its own procedure will help clarify both its logic and that of the procedure that calls it. Using magic numbers instead of constants in code has always been bad practice; however, we have to consider any memory constraint criteria. In this case, maintainability has been compromised in favor of size. If you choose to make this kind of compromise, try to keep the code simple. Whenever you develop complex logic, always ensure that there are plenty of comments, because code that might be easy to understand a day after you've written it has a habit of becoming quite complex three months later.

Often your application will need to use external files and resources such as Registry entries. Do not forget to consider the size of these elements when you start designing. You will need to think carefully about what you write to storage because this eats into the overall memory of the HPC. The Pontoon game does not use any Registry entries, but it does store the card graphics in bitmap files. We have 52 cards in our deck; each one has a corresponding bitmap file of 2022 bytes. Therefore, the overall storage space required is 105,144 bytes, or 103 KB. Our program file is 24 KB and we are using the PictureBox control, which is 59 KB. We can therefore calculate that the application will require a total of 183 KB. Because the HPC device has the Visual Basic CE run-time files in ROM, we do not need to include these components in our calculation. It would be too difficult to attempt to calculate the absolute total memory requirement because of the other influences, but so long as your program is small enough to run in the available memory and you have allowed for program terminations caused by insufficient memory, you should not have any problems.

An important point to be aware of is that on the desktop operating system each card would actually take up more space than its physical size; my PC, for example, will store each card in a space 32 KB in size. This size is called an allocation unit. Essentially, when writing files to disk, the operating system always write the data in blocks of a predetermined size (32 KB in my case). If I create a file less than or equal to 32 KB, the amount of disk space allocated for the file will actually be 32 KB. If the file's size is 33 KB, it will occupy two allocation units, or 64 KB. The allocation unit size on the PC is dependent upon the size of the hard disk partition and is configurable, but it can still be quite wasteful. Windows CE does not use allocation units, so my cards will occupy only the space equivalent to the file's actual size.

Programming for Windows CE

The Windows CE Toolkit for Visual Basic 5 is based on a subset of the Visual Basic, Scripting Edition programming language, so many Visual Basic language features available in other versions are not applicable or available when writing Windows CE programs. The Windows CE toolkit uses Visual Basic's code editor and syntax checker, and for this reason you will find that many of the errors caused by using features not available to the Windows CE environment will not be reported by the syntax checker. These errors are reported only at run time. Moreover, these run-time errors do not give specific information—they simply report that an error has occurred. When you run a Windows CE application in the development environment, the Visual Basic interpreter is not used—instead a debug version of your program is created and loaded either in the emulator environment or on the HPC device. Certain errors can be detected only after your application is executed. Once your program has started it has no further interaction with Visual Basic; instead, the Windows CE debugging window interacts with your program. Figure 5-12 illustrates how Visual Basic and the Windows CE toolkit components interact when you run a program. I will explain the emulator and the debugger in more detail later in this chapter.

In a Visual Basic Windows CE program you can create as many forms as you like, but you might have only one standard module. You cannot use class modules or any other type of module, such as User Documents or Property Pages, so you will not be able to create ActiveX components. You can, however, have related documents attached to your project. You need to be careful when using the properties and methods of standard Visual Basic objects, because many of the properties and methods either are not supported or have changed. The Knowledge Base contains articles providing a full list of what has changed or the excluded functionality, so I will not repeat them all here.

Figure 5-12 The Visual Basic debugging environment for Windows CE

What's new or changed in the language?

In addition to changes in the development environment, some new features and changes elsewhere have been added. The Books Online reference provides full documentation of the language, so I won't provide a comprehensive listing here. The following is a description of some elements that are either new or that work differently. Some elements are not new but might have missed your attention in previous versions, and they might now have a more prominent role in your designs.

One of the more common programming elements is the array. A big change here is that the lower bound of an array is always 0—this cannot be changed. A new set of functions has been included, which will make manipulation of arrays an easier task. The Array function was introduced in Visual Basic 5, but it might have gone unnoticed. Arrays will probably player a bigger role in your Windows CE code, so I'll describe the Array function.

Array function

The Array function takes a list of comma-separated arguments and returns an array containing the list of supplied arguments. The syntax of the Array function is variable = Array(arg1 [,arg2…]). The following code shows how you might use the Array function.

Dim ProductType
Dim Item

ProductType = Array("1 - Grocery", 1, "2 - Tobacco", 2, "3 -    Alcohol", 3)

For Each Item In ProductType
    If IsNumeric(Item) = False Then
        List1.AddItem Item
    Else
        List1.ItemData(List1.NewIndex) = Item
    End If
Next

In the example above, the variable ProductType is initially declared as a Variant. Assigning the result from the Array function causes a coercion to type Variant array. The bounds of the array are 0 to 5 because Windows CE supports only 0 as the lower bound of an array. The same result could be achieved using the more conventional coding technique of declaring the variable as an array and then assigning a value to each element, but the Array function is more efficient for small arrays.

The arguments of the Array function need not be hard-coded "constant" type values, as in our example above. Because the array created is a Variant array, you can use variables or even other arrays as arguments. The example below illustrates this.

Dim FirstArray
Dim SecondArray
Dim ThirdVariable

FirstArray = Array("A Value", "Another Value")
SecondArray = Array(FirstArray, "A Third Value")
ThirdVariable = SecondArray(0)
Print ThirdVariable(0)         ' Prints "A Value"
Print ThirdVariable(1)         ' Prints "Another Value"
Print SecondArray(1)           ' Prints "A Third Value"

When assigning arrays as elements of an array, remember that because the element is, in fact, an array, any variable you assign that element to will also be coerced to an array type. You can assign SecondArray(0) to another variable that would then, in fact, contain FirstArray, or you could interrogate the array in situ:

Print SecondArray(0)(0)        ' Prints "A Value"
Print SecondArray(0)(1)        ' Prints "Another Value"

For Each statement

The For Each programming construct should be familiar to nearly all programmers. With the increased support functions for array handling, you should be aware that the For Each construct can be used for arrays in addition to objects. (This has been available since Visual Basic 5.)

Dim ItemPrice
Dim Goods

Goods = Array(10, 12.54, 9.85)
For Each ItemPrice In Goods
    ItemPrice = ItemPrice + CalculateTaxOn(ItemPrice)
Next

In the example above, as the loop iterates, ItemPrice evaluates to the actual data in the array Goods for each element in turn.

CreateObject function

The CreateObject function is not new; in fact, it has been around for some time, but most Visual Basic programmers probably use the more familiar syntax Set X = New Object. The Windows CE Toolkit for Visual Basic 5 does not allow the declaration of API calls—the Declare keyword is no longer valid, nor is the New keyword. Therefore, it is now necessary to use the CreateObject function to instantiate instances of ActiveX (COM) objects.

If you have created objects in Visual Basic before, you might have noticed that the object reference held in HKEY_CLASSES_ROOT of the Registry identifies your object by ServerName.ClassName. Therefore, if you create an ActiveX component (say, CUSTOMER.DLL) with public classes of Account and History, the entry in HKEY_CLASSES_ROOT would contain the entries shown in Figure 513.

Figure 5-13 Objects are identified by ServerName.ClassName

Although you cannot build objects for Windows CE using Visual Basic, you can still use objects created in another language like Visual C++ 5 with the Windows CE Toolkit. This can be particularly useful because you have the ability to create C++ objects that wrap API functionality.

The syntax of the CreateObject function is CreateObject(ServerName.ClassName). The following code shows how you would normally use this function to create a COM object instance.

Dim WinCeDB
WinCeDB = CreateObject("WinCeAPI.DataBaseFunctions")
WinCeDB.OpenDatabase Id, "My Database", 0, 0, vbNull

Some Microsoft applications like Word and Excel expose objects that you can use. I would strongly recommend using these and other Microsoft objects where possible. I would also expect a plethora of third-party objects to hit the market shortly, though you should apply your usual testing methods before using any of these.

For Next statement

A minor change has been made to the For Next construct. In Windows CE development you are not allowed to specify the counter variable on the Next line. The code

For Counter = 1 to 100
    .
    .
    .
Next Counter

is invalid and will produce an error. The code would have to be written as

For Counter = 1 to 100
    .
    .
    .
Next

in order to work.

String functions

Visual Basic string functions can no longer be used with the type suffix. Because the language supports only Variant data types, you will need to use the Variant functions instead, such as Mid instead of Mid$.

File handling

File handling is an intrinsic part of many applications. However, the file handling routines you are probably used to are not included in the language. Instead, a new ActiveX control is provided that wraps all the file functionality. The FileSystem component adds two controls to the toolbox: the FileSystem control and the File control. To use these you need to place them on a form.

The FileSystem control This control provides the means to manipulate the file system, such as creating and deleting files, searching, copying, and getting or setting file attributes. The following code snippet shows how you would use a FileSystem control (fs) to fill a list box with file names.

Do
    If sFile = "" Then sFile = fs.Dir(sPath & "*.*") Else sFile = fs.Dir
    If sFile = "" Then Exit Do
    List1.AddItem sPath & sFile
Loop

The File control Whereas the FileSystem control provides the functionality to manipulate the file system, the File control allows you to create, read, and write file data. This example writes data to a random access file.

Dim vData

Const REC_LEN = 20

Const ModeInput = 1:  Const LockShared = 1:   Const AccessRead = 1
Const ModeOutput = 2: Const LockRead = 2:     Const AccessWrite = 2
Const ModeRandom = 4: Const LockWrite = 3:    Const AccessReadWrite = 3
Const ModeAppend = 8: Const LockReadWrite = 5
Const ModeBinary = 32

vData = Array("Chris", "John", "Julie")

fl.Open "My File", ModeRandom, AccessReadWrite, LockShared, REC_LEN
fl.Put vData(0), 1    ' Write record 1
fl.Put vData(1), 2    ' Write record 2
fl.Put vData(2), 3    ' Write record 3
fl.Close

Language objects

The Windows CE Toolkit for Visual Basic 5 language has seven objects. These are:

  • App

  • Clipboard

  • Err

  • Finance

  • Font

  • Form

  • Screen

Of these objects, the Finance object is new. The other objects are Windows CE implementations with reduced functionality from other Visual Basic versions. The Finance object, as you might expect, provides financial functions, but you must create this object yourself in order to use it, as you can see below:

Dim oFinance
Set oFinance = CreateObject("pVBFinFunc.pVBFinFunc")
Text1.Text = oFinance.Pmt(0.0083, 48, 12000)

I would recommend that you study the Books Online carefully to determine exactly the functionality of these objects.

Dealing with Errors

In terms of error handling you have two choices: don't, or use On Error Resume Next. You cannot use line numbers or labels in your code, so On Error GoTo would not work anyway. If you've been reading the various error handling articles produced by both Peet Morris and me, you are no doubt aware of the broad strategy of inserting generic catch-all error handling code throughout your application and then writing more specific handlers for anticipated errors. With the reduced error handling capabilities, a good scheme for Windows CE applications is to omit the generic handlers and just code for anticipated errors. This is because the Windows CE error handling mechanism has also been changed. If an unhandled error occurs, Visual Basic will display the default error message box; unlike other versions of Visual Basic, however, the program will not be terminated. Instead, the call stack is cleared and your application returns to an idle state (the state you were in before the last event was invoked). Beware! Your application is now in a stable state, not a known state. If you have written some data or manipulated some variables before the error occurred, it is possible that your application will actually be in an invalid state. The following pseudocode illustrates this:

Private Sub PurchaseItem_Click()
    aShoppingBasket = GetShoppingBasketList
    aCustomerAcct = GetCustomerAccount
    aShipList = GetShipList
    For Each Item In aShoppingBasket
        If aCustomerAcct(CustomerBalance) _ Item(Price) > 0 Then
            AddToShipList Item(ItemCode), aCustomerAcct(CustomerNumber)
           *** ERROR ****
            DeductFromCustomer Item(ItemPrice), _
                               aCustomerAcct(CustomerNumber)
        End If
    Next
End Sub

In this example the unhandled error causes the procedure to exit, and code after the error will not be executed. If the For Each loop had already performed some iterations before the error, you would effectively have a situation where some of the orders had been placed, but not all of them. The shopping basket data would still contain both processed and unprocessed orders, which means that if the procedure were invoked again for the same customer, you would have doubled some of the orders. You can prevent errors such as this by changing the design of your program. For example, in the code above, a better idea would be to remove the item from the shopping basket immediately after an individual order has been placed, and then write a specific error handler around the transaction.

The default error message box displayed by Windows CE is shown in Figure 5-14. The error message varies depending on where the error has occurred; in this example, the error has occurred after the application's form has loaded. Errors in the Form_Load event procedure are a little harder to trap. This is because the Form_Load event procedure executes before the debugger is loaded. You cannot, therefore, set a breakpoint to trap the error. Remember that Visual Basic debugging is not integrated with the Windows CE environment, so the Break options have no effect. An error that occurs while running your application does not cause the program to break into debug mode. The only way to trap an error is to set a breakpoint in the debugger and then step through the code until the error is reached.

Figure 5-14 An unhandled error message from Visual Basic for Windows CE

The Pontoon application does not contain any error handlers. At design time and during coding, I evaluated the possibility of an error occurring. In this application there are no places where I can anticipate an error, so it would be a waste to code error handlers everywhere just in case. Remember that using On Error Resume Next requires lots of code to implement correctly because you need to check for an error after every line where an error could possibly occur. Another disadvantage is that because you cannot have line labels, you will effectively end up having deeply-nested conditionals. When determining your error handling requirements, it is obviously important to consider how critical the application is. If the Pontoon game has an error, the consequences are not severe. The Err object is available for diagnosing errors, although the Erl property is not present for obvious reasons.

If these limitations seem a little restrictive, I'll remind you again of the types of applications you will be creating at this stage. As Microsoft's goal to allow development for any platform in Visual Basic gets closer to reality and the hardware's capacity grows, I expect the error handling capabilities will grow to comparable levels with that of other versions of Visual Basic (or maybe even C++!).

The Windows CE Desktop Emulator

The Windows CE Desktop Emulator is a program supplied with the Windows CE Platform Software Development Kit (and also supplied with the Windows CE toolkit). The emulator is a program that runs the Windows CE operating system on your desktop. You can start the emulator by running the program manually, or it is started automatically when you run a Windows CE application from Visual Basic after having selected the target device as Emulator. You can write, test, and run your applications wholly within the emulator without having a physical HPC device at all. The Start menu of the emulator is really a dummy menu—you cannot use it to access any of the programs loaded in the emulator. To browse and copy files within the emulator's environment, you need to run the My Handheld PC program from the desktop. Even from here not all the programs will run in the emulator, though your Visual Basic programs will. The emulator's file system is stored in the object store (essentially an OBS file), meaning that you cannot copy or move files between your PC and the emulator using standard methods like drag-and-drop. To do this you need to use the program EMPFILE.EXE, which you can find in the \wce\emul\hpc\Windows folder in the Windows CE Platform SDK folder. The location of the Windows CE Platform SDK folder obviously depends on your installation options. The following listing shows the Help information that the program displays when run from an MS-DOS window:

C:\Program Files\Windows CE Platform SDK\wce\emul\hpc\windows>empfile
USAGE:  EMPFILE [options]...

options:
 -c SOURCE DEST  ('put' or 'get' a file from object store)
 -d FILE         (delete a file on object store)
 -e FILE         (check to see if a file exists on object store)
 -s              (synchronize object store with wce\emul\PLATFORM\ tree)
 -r MYAPP ARGS   (run myapp.exe in the object store with arguments ARGS)
examples:
   EMPFILE -s
           (Synchronize wce\emul\PLATFORM\ tree with object store)
   EMPFILE -c c:\test.exe wce:windows\test.exe
           (Copy c:\test.exe to object store's Windows\ directory)
   EMPFILE -c wce:test.exe c:\test.exe
           (Copy test.exe from object store to c:\)
   EMPFILE -e windows\text.exe
           (verify that test.exe Exists in object store's
           Windows\ directory)
   EMPFILE -d test.txt
           (Delete test.txt from object store root directory)
   EMPFILE -r test.exe -10
           (Run test.exe from object store with parameter "-10")

You can use EMPFILE to copy files to and from the emulator and also to synchronize databases between the emulator and your PC. If you write any programs that require external files (such as bitmaps), you need to use EMPFILE to copy those files to the emulator so that these files are available when you run your program under the emulator.

Testing and Debugging Your Application

The limitations of the current version of the Windows CE toolkit mean that you will have to thoroughly test and debug your code, even more so than you do now. The debugger allows you to debug applications running either in the emulator or on the HPC. Before running your application at design time, you will first need to download the run-time files. This option is available under the Windows CE menu. This process downloads the Visual Basic run-time library files to the emulator environment and to the HPC. Once completed, you will need to be sure that you download the controls required by your application by selecting Control Manager from the Windows CE menu. Figure 5-15 shows the Control Manager screen.

Figure 5-15 Windows CE Control Manager

You need to download only the controls you are using in your project, but they must be downloaded to either the emulator or to the HPC depending on the target you have selected. Select the controls to download and use the Emulation or Device menus to install or uninstall the controls. Once you have completed these steps you can run your program in the normal way—using F5 or the Run command.

Running a Windows CE application in the development environment is a little slower than normal, because the program is downloaded and run within the selected environment. The debugger application interfaces with that environment. The debug facility offers a reduced set of functionality compared to other versions of Visual Basic. Once in the debugger you can set breakpoints, but you cannot change any code. The debugger supports the same debug actions as in other versions. Figure 5-16 shows the debug window.

The biggest difference between this debug window and that which you will become accustomed to is that you cannot change code within it. To change your code you will need to stop the application, close the debugger, amend your code, and then rerun it. Unfortunately, if you keep the debug window open after stopping the application, you need to set your breakpoints again when you restart another instance of the debugger—there's no way around it!

Figure 5-16 The debug window

Run-time errors in the development environment are dealt with differently than what you might be used to. Misspelled variable or function names and parameter constants are not all detected until the procedure is entered. One tip for avoiding these errors is to always type these elements in lowercase. If valid, the capitalization will be changed to that of the declaration; if not, the case will remain unchanged. This will give you a good indication that a variable name is correct.

Sometimes you might find the debug window a distraction. If this happens, you can prevent the debug window from being displayed by deselecting the Build Debug option on the Make tab of the Project Properties dialog box.

Deploying Your Application

You can get your application onto the HPC device in one of two ways, both of which are equally as easy. For one-off installation you can simply copy the application and its data files to the HPC using the Mobile Devices folder. The Mobile Devices folder works in the same way as Windows Explorer, and you can drag-and-drop files as well as create folders and shortcuts. You can also access your HPC device directly through Windows Explorer. Before copying your application you will need to compile it using the Make option, just as with a regular Visual Basic application. There are no optimization options because the compiled program is rather like a P-Code executable. The compiled application has a PVB file extension. If you are using any custom controls, you will need to install these onto the device using the Control Manager, and you will need to register ActiveX components using the REGSVRCE.EXE program (the Windows CE equivalent of REGSVR32.EXE).

For a more formal installation, you will need to use the Application Install Wizard (available from the Windows CE menu), which works in a similar way to the Setup Wizard in other versions of Visual Basic. Installation programs for Windows CE applications follow the same format. When installing from a setup program, the setup checks the status of your device and then prompts you to select the components you want to install. The application's setup files are extracted to a temporary storage area on the HPC. At this point, the PC's work is all but done and you are prompted to check your device for further messages. As soon as the PC has finished the copy process, the HPC starts installing the files. This is normally a fairly quick process; your application is then ready to run.

Extending Visual Basic Using COM DLLs

The Windows CE toolkit does not allow you to declare API functions, but you can use the CreateObject function to create instances of ActiveX components. Therefore, if you want to use any of the Windows CE API functions, you will need to create an ActiveX object that wraps the functionality required. Before embarking on this task, I would advise that you check the necessity for the particular API functions you want to use. Remember that the operating system itself contains only a subset of the Win32 API. Whereas the Win32 API has some 15,000 functions, the Windows CE API has only around 1200—the function you require might not even exist. I can recommend the book Microsoft Windows CE Programmer's Guide (Microsoft Press) as an excellent reference. This book gives complete documentation of the Windows CE API and covers topics such as communications and file and Registry access.

There are two "legitimate" areas of functionality for which you might decide to write ActiveX components. The first is the ActiveSync functionality. The Windows CE databases can be synchronized with a PC using ActiveSync. When your HPC performs its synchronization, what it is actually synchronizing are the databases on your device. The HPC is shipped with the Contacts, Calendar, Inbox, and other databases. In addition, you can create your own databases using the HPC Database application that comes with most HPC devices. The HPC Database application is normally found in the Databases icon on the desktop. You can configure ActiveSync to maintain synchronized copies of any or all these databases. Using the API you can achieve this functionality in code, which is useful if you write applications incorporating the Windows CE database functionality.

The database API functions are another area for which you might want to write wrappers. The API gives you the ability to create, open, enumerate, read, write, and delete the Windows CE database files.

To create an ActiveX wrapper you will need Visual C++ 5 and the Windows CE Toolkit for Visual C++ 5. Using this combination you can create a DLL that implements an Active Template Library (ATL) COM object, and then create instances of that object in your Visual Basic program by using the CreateObject function.


Chapter 6 -- Staying in Control

Chapter 6

Staying in Control

Effective Weapons in the War Against Bugs

MARK PEARCE

Mark is a TMS Associate who has been programming professionally for the past 19 years, working mainly in the investment banking industry. In recent years, Mark has concentrated on the design and development of effective client/server systems. His interest in zero-defect software comes from a previous incarnation as a professional chess player, where one mistake can lose you your next meal. Mark's current ambitions include making more money than he spends, not ever learning any flavor of Java, and finding the perfect mountain running trail.

"At least one statement in this chapter is wrong (but it may be this one)."

Peter Van Der Linden
(paraphrased)

Program bugs are highly ecological because program code is a renewable resource. If you fix a bug, another will grow in its place. And if you cut down that bug, yet another will emerge; only this one will be a mutation with long, poisonous tentacles and revenge in its heart, and it will sit there deep in your program, cackling and making elaborate plans for the most terrible time to strike.

Every week seems to bring Visual Basic developers more powerful but also more complex language features, custom controls, APIs, tools, and operating systems. While Visual Basic 5 finally brought us a "grown-up" language with real enterprise pretensions, Visual Basic 6 arrives only a year later, along with a whole new raft of acronyms and concepts. The phrase "technological downpour," coined by a Microsoft executive, strikes a chord with both developers and their managers. In the midst of all this technological chaos, the deadlines become tougher and our tools often refuse to cooperate with one another. If whatever we build lasts at least until we've finished building it, we consider it an unexpected bonus. Yet we are expected to sculpt stable Microsoft Visual Basic code that gives our users more flexible, less costly, and easier-to-use systems.

From this chapter's point of view, the key word is "stable." It's no use being able to churn out ever larger and more capable systems with these new, improved, wash-whiter-than-white tools if all that we succeed in doing is producing more defects. Developers tend to take a casual attitude toward bugs. They know them intimately, including their origin and even their species. A typical programmer looks at bugs in the same manner as an Amazonian tribe member looks at the insect-infested jungle—as an inevitable fact of life. The typical user is more like a tourist from the big city stranded in the same jungle. Surrounded by hordes of disgustingly hairy creepy-crawlies with too many legs and a nasty habit of appearing in the most unexpected places, the user often becomes upset—which is hardly surprising. This different perspective is one that software developers need to consider if they expect to meet user expectations.


An Expensive Tale

Production bugs are expensive, often frighteningly so. They're expensive in monetary terms when it comes to locating and fixing them. They're expensive in terms of data loss and corruption. And they're expensive when it comes to the loss of user confidence in your software. Some of these factors can be difficult to measure precisely, but they exist all the same. If we examine the course of a typical production defect in hard monetary terms alone, we can get some idea of the magnitude of costs involved when developers allow bugs into their master sources.

Enter Erica, a foreign exchange dealer for a major investment bank, who notices that the software she is using to measure her open U.S. dollar position seems to be misreporting the value of certain trades. Luckily she spots the defect quickly, before any monetary loss is incurred. Being distrustful of the new software, she has been keeping track of her real position on her trade blotter, so the only real cost so far is the time she has had to devote to identifying the problem and proving that it exists. But in that time, she has lost the opportunity to make an advantageous currency trade. Defect cost so far: $5,000.

Peter, a long-time programmer in the bank's Information Systems (IS) department, is given the task of finding and fixing the defect. Although Peter is not very familiar with the software in question, the original developer's highly paid contract ended a week ago, and he's lying on a beach in Hawaii. Peter takes a day to track down the bug (a misunderstanding about when Visual Basic triggers the LostFocus event of a text box) and another day to fix the program, test his fix, and run some regression tests to ensure that he has not affected any other part of the program. Defect cost so far: $6,000.

Sally is asked to check Peter's work and to write up the documentation. She notices that the same problem occurs in four other programs written by the contractor and tells Peter to fix those programs too. The fixes, testing, and documentation take another three days. Defect cost so far: $9,000.

Tony in the Quality Assurance (QA) department is the next person to receive the amended programs. He spends a day running the full set of QA standard tests. Defect cost so far: $10,000.

Finally, Erica is asked to sign off the new release for production. Because of other pressing work, she performs only the minimal testing needed to convince herself that she is no longer experiencing the same problem. The bug fix now has all the signatures necessary for production release. Total defect cost: $11,000.

But wait: statistics show that some 50 percent of bug fixes lead to the introduction of at least one new bug, which brings the statistical cost of this particular bug to over $16,000! This amount doesn't include the overhead costs of production support and implementation.

This example of a typical defect found in a production environment illustrates that the financial expenses involved in finding and fixing software bugs are often large. A commonly accepted figure in the information technology (IT) industry is that this kind of problem costs an order of magnitude more at each stage of the process. In other words, if the bug in our particular example had been found by the programmer during development, it might have cost $16 to fix. Found by a tester, the cost would have been around $160. Found by a user before it had gone into production, the cost might have been $1,600. Once the problem reaches production, the potential costs are enormous.

The most expensive bug ever reported (by Gerald Weinberg in 1983), the result of a one-character change in a previously working program, cost an incredible $1.6 billion. The jury is still out on the true cost of the Year 2000 bug, but current estimates are in the region of $600 billion worldwide. (See Chapter 8 for an in-depth treatment of the Y2K problem.) Intel spent $200 million to compensate PC owners for the now notorious Pentium bug. In 1992, a fly-by-wire passenger plane crashed during an air show, killing eleven people. The crash was traced to a bug in the software controlling the plane's flight systems. A total failure caused by bugs in a new software system installed to control the dispatch of London ambulances was judged to have directly contributed to at least one patient's death. In 1996, a London brokerage house had to pay more than $1 million to its customers after new software failed to handle customer accounts properly. The list goes on and on.

Most bugs don't have such life-or-death effects; still, the need for zero-defect or low-defect software is becoming increasingly important to our civilization. We have everything from nuclear power stations to international banking systems controlled by software, making bugs both more dangerous and more expensive. This chapter is about techniques that help to reduce or eliminate bugs in production code, especially when writing Visual Basic 6 programs.


What Are We Trying to Accomplish?

The aim of this chapter is to teach you different methods of catching bugs during Visual Basic 6 program development and unit testing, before they can reach the master source files. As programmers, we are in a unique position. First, we can learn enough about bugs and their causes to eliminate large classes of them from our programs during initial development. Second, we are probably the only people who have sufficient knowledge of the internal workings of our programs to unit-test them effectively and thereby identify and remove many bugs before they ever reach our testers and users. Developers tend to be highly creative and imaginative people. The challenge is to impose a certain discipline on that talent in order to attack the bug infestation at its very source. Success in meeting this challenge will give us increased user confidence, fewer user support calls and complaints, shorter product development times, lower maintenance costs, shorter maintenance backlogs, and increased developer confidence—not to mention an ability to tamper with the reality of those developers who think that a zero-defect attitude to writing code is nonproductive.

A Guided Tour

In the first part of this chapter, we'll take a look at some of the more strategic issues involved in the high bug rate currently experienced by the IT industry. We'll also look at some of the latest ideas that leading software companies such as Microsoft and Borland use to tackle those issues. Although these ideas aren't all directly related to writing code, Visual Basic developers and their managers need to understand them and the issues behind them. As Visual Basic 6 becomes more and more the corporate tool of choice in the production of large-scale projects, we are faced with a challenge to produce complex, low-defect systems within reasonable schedules and budgets. Without a firm strategic base on which to build, the game will be lost even before we start designing and coding.

We'll also examine the role that management and developer attitudes play in helping to produce fewer bugs. One of the key ideas here is that most program bugs that reach production can be avoided by stressing the correct software development attitudes. Several studies have shown that programming teams are successful in meeting the targets they set, provided these targets are specific, nonambiguous, and appropriately weighted in importance for the project being tackled. The attitudes of developers are driven by these targets, and we'll look at ways of reinforcing the attitudes associated with low bug rates.

Then it will be time to get our hands dirty. You probably remember those medieval maps that used to mark large empty regions with the phrase "Here Be Dragons". We're going to aim for their Visual Basic 6 equivalent, boldly venturing into the regions labeled "Here Be Nasty Scaly Six-Legged Hairy Bugs" and looking at some issues directly related to Visual Basic design and coding. We'll see where some of the more notorious and ravenous bugs are sleeping and find out how we can avoid waking them—or at least how we can avoid becoming really tangled up in them. At this point, we'll sometimes have to delve into rather technical territory. This journey into technical details is unfortunately inevitable when peering at creatures worthy of some of H. R. Giger's worst creations. Once you come out on the other side unharmed, you should have a much better appreciation of when and where Visual Basic developers have to be careful.

In the final section of this chapter, we'll look at some tools that can aid the bug detection and prevention processes in several ways. Microsoft seems to have established a virtual monopoly on the term "Wizard" to describe an add-in or utility designed to help programmers with some aspect of code development. So casting around for a suitable synonym, I came up with "Sourcerer" (thanks, Don!) instead, or perhaps Sourceress. Three such tools are demonstrated and explained.

The three sourcerers

The first tool is the Assertion Sourcerer, an add-in that supplements Visual Basic 6's Debug.Assert statement and allows you to implement assertions even in compiled modules, ideal for testing distributed components. Next comes the Metrics Sourcerer, also an add-in. It uses a couple of fairly simple measurements to estimate the relative complexity of your Visual Basic 6 project's procedures, forms, and classes. Several studies have shown that the longer and more complex a procedure, the more likely it is to have bugs discovered in it after being released to production. The final utility is the Instrumentation Sourcerer, yet another add-in. It adds instrumentation code to your Visual Basic 6 project to track all user interactions with your application's graphical interface. This tool can be invaluable in both tracking a user's actions leading up to that elusive program bug and showing exactly how different users use your application in the real world.

"Some final thoughts" sections

Throughout this chapter, many sections end with a recommendation (entitled "Some Final Thoughts") culled from both my own experiences and those of many other people in the IT industry. Acting on these suggestions is probably less important than understanding the issues behind them, as discussed in each section. These recommendations are just opinions, candidly stated, with no reading between the lines required.


Some Strategic Issues

Before we take a closer look at Visual Basic 6, we need to examine several general factors: priorities, technological progress, and overall project organization. Without understanding and controlling these factors, the best developers in the world can't avoid producing defects. These issues are not really Visual Basic 6-specific. Their effect is more on the whole development process. To extend the bug/beastie analogy onto even shakier ground, these are the real gargoyles of the bug world. Their presence permeates a whole project, and if left unrecognized or untamed they can do severe and ongoing damage.

Priorities: The Four-Ball Juggling Act

Software development is still much more of an art than a science. Perhaps one area in which we can apply a discipline more reminiscent of normal engineering is that of understanding and weighing the different aspects of a project. In almost any project, four aspects are critical:

  1. The features to be delivered to the users

  2. The hardware, software, and other budgets allocated to the project

  3. The time frames in which the project phases have to be completed

  4. The number of known defects with which the project is allowed to go into production

Balancing these four factors against one another brings us firmly into the realm of classical engineering trade-offs. Concentrating on any one of these aspects to the exclusion of the others is almost never going to work. Instead, a continuous juggling act is required during the life of most projects. Adding a new and complicated feature might affect the number of production bugs. Refusing to relax a specific project delivery date might mean reducing the number of delivered features. Insisting on the removal of every last bug, no matter how trivial, might significantly increase the allocated budgets. So the users, managers, and developers make a series of decisions during the life of a project about what will (or won't) be done, how it will be done, and which of these four aspects takes priority at any specific time.

The major requirement here from the zero-defect point of view is that all the project members have an explicit understanding about the relative importance of each of these aspects, especially that of production bugs. This consensus gives everybody a framework on which to base their decisions. If a user asks for a big new feature at the very end of the project, he or she has no excuse for being unaware of the significant chance of production bugs associated with the new feature, or of the budget and schedule implications of preventing those bugs from reaching production. Everybody will realize that a change in any one of these four areas nearly always involves compromises in the other three.

A project I was involved with some time ago inherited a legacy Microsoft SQL Server database schema. We were not allowed to make any significant structural changes to this database, which left us with no easy way of implementing proper concurrency control. After considering our project priorities, we decided to do without proper concurrency control in order to be able to go into production on the planned date. In effect, we decided that this major design bug was acceptable given our other constraints. Knowing the original project priorities made it much easier for us to make the decision based on that framework. Without the framework, we would have spent significant time investigating potential solutions to this problem at the expense of the more important project schedules. When pointed out in black and white, our awareness of the project's priorities seems obvious. But you'd be surprised at the number of projects undertaken with vague expectations and unspecified goals. Far too often, there is confusion about exactly which features will be implemented, which bugs will be fixed, and how flexible the project deadlines and budgets really are.

Some final thoughts Look at your project closely, and decide the priorities in order of their importance. Determine how important it is for your project to go to production with as few bugs as possible. Communicate this knowledge to all people involved in the project, including the users. Make sure that everyone has the framework in which to make project decisions based on these and other priorities.

Progress Can Be Dangerous

Avalanches have caused about four times as many deaths worldwide in the 1990s as they did in the 1950s. Today, in spite of more advanced weather forecasting, an improved understanding of how snow behaves in different climatic conditions, and the use of sophisticated locating transmitters, many more people die on the slopes. In fact, analysis shows that the technological progress made over the last four decades has actually contributed to the problem. Skiers, snowboarders, and climbers are now able to roam into increasingly remote areas and backwoods. The wider distribution of knowledge about the mountains and the availability of sophisticated instruments have also given people increased confidence in surviving an avalanche. While many more people are practicing winter sports and many more adventurers have the opportunity to push past traditional limits, the statistics show that they have paid a heavy price.

In the same way that technological progress has ironically been accompanied by a rise in the number of avalanche-related deaths, the hot new programming tools now available to developers have proved to be a major factor in the far higher bug rates that we are experiencing today compared to five or ten years ago. Back in the olden days of Microsoft Windows programming (about 1990 or so), the only tools for producing Windows programs were intricate and difficult to learn. Only developers prepared to invest the large amounts of time required to learn complex data structures and numerous application programming interface (API) calls could hope to produce something that even looked like a normal Windows program. Missing the exact esoteric incantations and laying on of hands, software developed by those outside an elite priesthood tended to collapse in a heap when its users tried to do anything out of the ordinary—or sometimes just when they tried to run it. Getting software to work properly required developers to be hardcore in their work—to understand the details of how Windows worked and what they were doing at a very low level. In short, real Windows programming was often seriously frustrating work.

With the introduction of Microsoft Visual Basic and other visual programming tools, a huge amount of the grunt work involved in producing Windows programs has been eliminated. At last, someone who hasn't had a great deal of training and experience can think about producing applications that have previously been the province of an elite group. It is no longer necessary to learn the data structures associated with a window or the API calls necessary to draw text on the screen. A simple drag-and-drop operation with a mouse now performs the work that previously took hours.

The effect has been to reduce dramatically the knowledge levels and effort needed to write Windows programs. Almost anybody who is not a technophobe can produce something that resembles, at least superficially, a normal Windows program. Although placing these tools into the hands of so many people is great news for many computer users, it has led to a startling increase in the number of bug-ridden applications and applications canceled because of runaway bug lists. Widespread use of these development tools has not been accompanied by an equally widespread understanding of how to use them properly to produce solid code.

What is necessary to prevent many types of defects is to understand the real skills required when starting your particular project. Hiring developers who understand Visual Basic alone is asking for trouble. No matter how proficient programmers are with Visual Basic, they're going to introduce bugs into their programs unless they're equipped with at least a rudimentary understanding of how the code they write is going to interact with all the other parts of the system. In a typical corporate client/server project, the skills needed cover a broad range besides technical expertise with Visual Basic. Probably the most essential element is an understanding of how to design the application architecture properly and then be able to implement the architecture as designed. In the brave new world of objects everywhere, a good understanding of Microsoft's Component Object Model (COM) and of ActiveX is also essential. In addition, any potential developer needs to understand the conventions used in normal Windows programs. He or she must understand the client/server paradigm and its advantages and disadvantages, know an appropriate SQL dialect and how to write efficient stored procedures, and be familiar with one or more of the various database communication interfaces such as Jet, ODBC, RDO, and ADO (the VB interface to OLE DB). Other areas of necessary expertise might include knowledge about the increasingly important issue of LAN and WAN bandwidth and an understanding of 16-bit and 32-bit Windows architecture together with the various flavors of Windows APIs. As third-party ActiveX controls become more widespread and more complex, it might even be necessary to hire a developer mainly for his or her expertise in the use of a specific control.

Some final thoughts You don't hire a chainsaw expert to cut down trees—you hire a tree surgeon who is also proficient in the use of chainsaws. So to avoid the serious bugs that can result from too narrow an approach to programming, hire developers who understand client/server development and the technical requirements of your specific application, not those who only understand Visual Basic.

Dancing in Step

One of the most serious problems facing us in the battle against bugs is project size and its implications. As the size of a project team grows linearly, the number of communication channels required between the team members grows factorially (in fact, almost exponentially once the numbers reach a certain level). Traditionally, personal computer projects have been relatively small, often involving just two or three people. Now we're starting to see tools such as Visual Basic 6 being used in large-scale, mission-critical projects staffed by ten to twenty developers or more. These project teams can be spread over several locations, even over different continents, and staffed by programmers with widely varying skills and experience.

The object-oriented approach is one attempt to control this complexity. By designing discrete objects that have their internal functions hidden and that expose clearly defined interfaces for talking to other objects, we can simplify some of the problems involved in fitting together a workable application from many pieces of code produced by multiple developers.

However, programmers still have the problems associated with communicating what each one of hundreds of properties really represents and how every method and function actually works. Any assumptions a programmer makes have to be made clear to any other programmer who has to interact with the first programmer's objects. Testing has to be performed to ensure that none of the traditional implementation problems that are often found when combining components have cropped up. Where problems are found, two or more developers must often work together for a while to resolve them.

In an effort to deal with these issues, which can be a major cause of bugs, many software companies have developed the idea of working in parallel teams that join together and synchronize their work at frequent intervals, often daily. This technique enables one large team of developers to be split into several small teams, with frequent builds and periodic stabilization of their project. Small teams traditionally have several advantages over their larger counterparts. They tend to be more flexible, they communicate faster, they are less likely to have misunderstandings, and they exhibit more team spirit. An approach that divides big teams into smaller ones but still allows these smaller groups to synchronize and stabilize their work safely helps to provide small-team advantages even for large-team projects.

What is the perfect team size? To some extent, the optimum team size depends on the type of project; but studies typically show that the best number is three to four developers, with five or six as a maximum. Teams of this size communicate more effectively and are easier to control.

Having said this, I still think you need to devise an effective process that allows for the code produced by these small teams to be combined successfully into one large application. You can take several approaches to accomplish this combination. The process I recommend for enabling this "dancing in step," which is similar to the one Microsoft uses, is described here:

  1. Create a master copy of the application source. This process depends on there being a single master copy of the application source code, from which a periodic (often daily) test build will be generated and released to users for testing.

  2. Establish a daily deadline after which the master source cannot be changed. If nobody is permitted to change the master source code after a certain time each day, developers know when they can safely perform the synchronization steps discussed in detail in the rest of these steps.

  3. Check out. Take a private copy of the code to be worked on from the master sources. You don't need to prevent more than one developer from checking out the same code because any conflicts will be dealt with at a later stage. (See step 8.)

  4. Make the changes. Modify the private copy of the code to implement the new feature or bug fix.

  5. Build a private release. Compile the private version of the code.

  6. Test the private release. Check that the new feature or bug fix is working correctly.

  7. Perform pretesting code synchronization. Compare the private version of the source code with the master source. The current master source could have changed since the developer checked out his or her private version of the source at the start of this process. The daily check-in deadline mentioned in step 2 ensures that the developers know when they can safely perform this synchronization.

  8. Merge the master source into the private source. Merge the current master source into the private version of the source, thus incorporating any changes that other developers might have made. Any inconsistencies caused by other developers' changes have to be dealt with at this stage.

  9. Build a private release. Build the new updated private version of the source.

  10. Test the private release. Check that the new feature or bug fix still works correctly.

  11. Execute a regression test. Test this second build to make sure that the new feature or bug fix hasn't adversely affected previous functionality.

  12. Perform pre-check-in code synchronization. Compare the private version of the source code with the master source. Because this step is done just prior to the check-in itself (that is, before the check-in deadline), it will not be performed on the same day that the previous pretesting code synchronization (which occurs after the check-in deadline; see step 7) took place. Therefore, the master source might have changed in the intervening period.

  13. Check in. Merge the private version of the source into the master source. You must do this before the daily check-in deadline mentioned in step 2 so that other developers can perform their private code synchronization and merges safely after the deadline.

  14. Observe later-same-day check-ins. It is essential that you watch later check-ins that day before the deadline to check for potential indirect conflicts with the check-in described in step 13.

  15. Generate a daily build. After the check-in deadline, build a new version of the complete application from the updated master sources. This build should be relatively stable, with appropriate punishments being allocated to project members who are responsible for any build breaks.

  16. Test the daily build. Execute some tests, preferably automated, to ensure that basic functionality still works and that the build is reasonably stable. This build can then be released to other team members and users.

  17. Fix any problems immediately. If the build team or the automated tests find any problems, the developer responsible for the build break or test failure should be identified and told to fix the problem immediately. It is imperative to fix the problem before it affects the next build and before that developer has the opportunity to break any more code. This should be the project's highest priority.

Although the above process looks lengthy and even somewhat painful in places, it ensures that multiple developers and teams can work simultaneously on a single application's master source code. It would be significantly more painful to experience the very frustrating and difficult bugs that traditionally arise when attempting to combine the work of several different teams of developers.

Some final thoughts Split your larger project teams into smaller groups, and establish a process whereby these groups can merge and stabilize their code with that of the other project groups. The smaller teams will produce far fewer bugs than will the larger ones, and an effective merging process will prevent most of the bugs that would otherwise result from combining the work of the smaller teams into a coherent application.


Some Attitude Issues

One of the major themes of this chapter is that attitude is everything when it comes to writing zero-defect code. Developers aren't stupid, and they can write solid code when given the opportunity. Provided with a clear and unambiguous set of targets, developers are usually highly motivated and very effective at meeting those targets. If management sets a crystal-clear target of zero-defect code and then does everything sensible to encourage attitudes aimed at fulfilling that target, the probability is that the code produced by the team will have few defects. So given the goal of writing zero-defect code, let's look at some of the attitudes that are required.

Swallowing a Rhinoceros Sideways

The stark truth is that there is no such thing as zero-defect software. The joke definition passed down from generation to generation (a generation in IS being maybe 18 months or so) expresses it nicely: "Zero defects [noun]: The result of shutting down a production line." Most real-life programs contain at least a few bugs simply because writing bug-free code is so difficult. As one of my clients likes to remind me, if writing solid code were easy, everybody would be doing it. He also claims that writing flaky code is much easier—which might account for the large quantity of it generally available.

Having said this, it is really part of every professional developer's job to aim at writing bug-free code. Knowing that bugs are inevitable is no excuse for any attitude that allows them the slightest breathing space. It's all in the approach. Professional programmers know that their code is going to contain bugs, so they bench-test it, run it through the debugger, and generally hammer it every way they can to catch the problems that they know are lurking in there somewhere. If you watch the average hacker at work, you'll notice something interesting. As soon as said hacker is convinced that his program is working to his satisfaction, he stops working, leans back in his chair, shouts to his boss that he's ready to perform a production release, and then heads for the soda machine. He's happy that he has spent some considerable time trying to show that his program is correct. Now fast-forward this hacker a few years, to the point where he has become more cynical and learned much more about the art of programming. What do you see? After reaching the stage at which he used to quit, he promptly starts working again. This time, he's trying something different—rather than prove his program is correct, he's trying to prove that it's incorrect.

Perhaps one of the major differences between amateur and professional developers is that amateurs are satisfied to show that their programs appear to be bug-free, whereas professionals prefer to try showing that their programs still contain bugs. Most amateurs haven't had enough experience to realize that when they believe their program is working correctly, they are perhaps only halfway through the development process. After they've done their best to prove a negative (that their code doesn't have any bugs), they need to spend some time trying to show the opposite.

One very useful if somewhat controversial technique for estimating the number of bugs still remaining in an application is called defect seeding. Before performing extensive quality assurance or user acceptance tests, one development group deliberately seeds the application code with a set of documented defects. These defects should cover the entire functionality of the application, and range from fatal to cosmetic, just as in real life. At the point when you estimate that the testing process is near completion, given the ratio of the number of seeded defects detected to the total number of defects seeded, you can calculate the approximate number of bugs in the application by using the following formula:

   D0 = (D1 / D2) * D3

D1 is the total number of seeded defects, D2 is the number of seeded defects found so far, and D3 is the number of real (i.e. non-seeded) defects found so far. The resulting figure of D0 is therefore the total number of defects in the application, and D0 minus D3 will give you the approximate number of real defects that still haven't been located.

Beware of forgetting to remove the seeded defects, or of introducing new problems when removing them. If possible, keep the seeded defect code encapsulated and thus easy to remove from the programs.

Some final thoughts Find developers who are intelligent, knowledgeable, willing to learn, and good at delivering effective code. Find out whether they are also aware that writing bug-free code is so difficult that they must do everything possible to prevent and detect bugs. Don't hire them without this final magical factor. It's true that the first four qualities are all wonderful, but they are meaningless without this last one.

Looping the Loop

One of the most effective ways of restraining soaring bug rates is to attack the problem at its source—the programmer. Programmers have always known about the huge gap between the quality of code produced by the best and by the worst programmers. Industry surveys have verified this folklore by showing that the least effective developers in an organization produce more than twenty times the number of bugs that the most effective developers produce. It follows that an organization would benefit if its better programmers produced the majority of new code. With that in mind, some corporations have introduced the simple but revolutionary idea that programmers have to fix their own bugs—and have to fix them as soon as they are found.

This sets up what engineers call a negative feedback loop, otherwise known as evolution in action. The more bugs a programmer produces, the more time he or she is required to spend fixing those bugs. At least four benefits rapidly become apparent:

  1. The more bugs a programmer produces, the less chance he or she has of working on new code and thereby introducing new bugs. Instead, the better programmers (judged by bug rate) get to write all the new code, which is therefore likely to have less bugs.

  2. Programmers soon learn that writing buggy code is counterproductive. They aren't able to escape from the bugs they've introduced, so they begin to understand that writing solid code on the first pass is more effective and less wasteful of time than having to go back to old code, often several times in succession.

  3. Bug-prone developers start to gain some insights into what it's like to maintain their own code. This awareness can have a salutary effect on their design and coding habits. Seeing exactly how difficult it is to test that extremely clever but error-prone algorithm teaches them to sympathize more with the maintenance programmers.

  4. The software being written has very few known bugs at any time because the bugs are being fixed as soon as they're found. Runaway bug lists are stomped before they can gather any momentum. And the software is always at a point where it can be shipped almost immediately. It might not have all the features originally requested or envisioned, but those features that do exist will contain only a small number of known bugs. This ability to ship at any point in the life of a project can be very useful in today's fast-changing business world.

Some people might consider this type of feedback loop as a sort of punishment. If it does qualify as such, it's an extremely neutral punishment. What tends to happen is that the developers start to see it as a learning process. With management setting and then enforcing quality standards with this particular negative feedback loop, developers learn that producing bug-free code is very important. And like most highly motivated personalities, they soon adapt their working habits to whatever standard is set. No real crime and punishment occurs here; the process is entirely objective. If you create a bug, you have to fix it, and you have to fix it immediately. This process should become laborious enough that it teaches developers how to prevent that type of bug in the future or how to detect that type of bug once it has been introduced.

Some final thoughts Set a zero-defect standard and introduce processes that emphasize the importance of that standard. If management is seen to concentrate on the issue of preventing bugs, developers will respond with better practices and less defects.

Back to School

Although Visual Basic 6 is certainly not the rottweiler on speed that C++ and the Microsoft Foundation Classes (MFC) can be, there is no doubt that its increased power and size come with their own dangers. Visual Basic 6 has many powerful features, and these take a while to learn. Because the language is so big, a typical developer might use only 10 percent or even less of its features in the year he or she takes to write perhaps three or four applications. It has become increasingly hard to achieve expertise in such a large and complex language. So it is perhaps no surprise to find that many bugs stem from a misunderstanding of how Visual Basic implements a particular feature.

I'll demonstrate this premise with a fairly trivial example. An examination of the following function will reveal nothing obviously unsafe. Multiplying the two maximum possible function arguments that could be received (32767 * 32767) will never produce a result bigger than can be stored in the long variable that this function returns.

Private Function BonusCalc(ByVal niNumberOfWeeks As Integer, _
    ByVal niWeeklyBonus As Integer) As Long

BonusCalc = niNumberOfWeeks * niWeeklyBonus

End Function

Now if you happened to be diligent enough to receive a weekly bonus of $1,000 over a period of 35 weeks…well, let's just say that this particular function wouldn't deliver your expected bonus! Although the function looks safe enough, Visual Basic's intermediate calculations behind the scenes cause trouble. When multiplying the two integers together, Visual Basic attempts to store the temporary result into another integer before assigning it to BonusCalc. This, of course, causes an immediate overflow error. What you have to do instead is give the Visual Basic compiler some assistance. The following revised statement works because Visual Basic realizes that we might be dealing with longs rather than just integers:

BonusCalc = niNumberOfWeeks * CLng(niWeeklyBonus)

Dealing with these sorts of language quirks is not easy. Programmers are often pushed for time, so they sometimes tend to avoid experimenting with a feature to see how it really works in detail. For the same reasons, reading the manuals or online help is often confined to a hasty glance just to confirm syntax. These are false economies. Even given the fact that sections of some manuals appear to have been written by Urdu swineherders on some very heavy medication, those pages still contain many pearls. When you use something in Visual Basic 6 for the first time, take a few minutes to read about its subtleties in the documentation and write a short program to experiment with its implementation. Use it in several different ways within a program, and twist it into funny shapes. Find out what it can and can't handle.

Some final thoughts Professional developers should understand the tools at their disposal at a detailed level. Learn from the manual how the tools should work, and then go beyond the manual and find out how they really work.

Yet More Schoolwork

Visual Basic 4 introduced the concept of object-oriented programming using the Basic language. Visual Basic 5 and 6 take this concept and elaborate on it in several ways. It is still possible to write Visual Basic 6 code that looks almost exactly like Visual Basic 3 code or that even resembles procedural COBOL code (if you are intent upon imitating a dinosaur). The modern emphasis, however, is on the use of relatively new ideas in Basic, such as abstraction and encapsulation, which aim to make applications easier to develop, understand, and maintain. Any Visual Basic developer unfamiliar with these ideas first has to learn what they are and why they are useful and then has to understand all the quirks of their implementation in Visual Basic 6. The learning curve is not trivial. For example, understanding how the Implements statement produces a virtual class that is Visual Basic 6's way of implementing polymorphism (and inheritance if you're a little sneaky) can require some structural remodeling of one's thought processes. This is heavy-duty object-oriented programming in the 1990s style. Trying to use it in a production environment without a clear understanding is a prime cause of new and unexpected bugs.

Developers faced with radically new concepts usually go through up to four stages of enlightenment. The first stage has to do with reading and absorbing the theory behind the concept. The second stage includes working with either code examples or actual programs written by other people that implement the new concept. The third stage involves using the new concept in their own code. Only at this point do programmers become fully aware of the subtleties involved and understand how not to write their code. The final stage of enlightenment arrives when the programmer learns how to implement the concept correctly, leaving no holes for the bugs to crawl through.

Some final thoughts Developers should take all the time necessary to reach the third and fourth stages of enlightenment when learning new programming concepts or methodologies. Only then should they be allowed to implement these new ideas in production systems.

Eating Humble Pie

Most developers are continually surprised to find out how fallible they are and how difficult it is to be precise about even simple processes. The human brain is evidently not well equipped to deal with problems that require great precision to solve. It's not the actual complexity but the type of complexity that defeats us. Evolution has been successful in giving us some very sophisticated pattern-recognition algorithms and heuristics to deal with certain types of complexity. A classic example is our visual ability to recognize a human face even when seen at an angle or in lighting conditions never experienced before. Your ability to remember and compare patterns means that you can recognize your mother or father in circumstances that would completely defeat a computer program. Lacking your ability to recognize and compare patterns intelligently, the program instead has to use a brute-force approach, applying a very different type of intelligence to a potentially huge number of possibilities.

As successful as we are at handling some sorts of complexity, the complexity involved in programming computers is a different matter. The requirement is no longer to compare patterns in a holistic, or all-around, fashion but instead to be very precise about the comparison. In a section of program code, a single misplaced character, such as "+" instead of "&," can produce a nasty defect that often cannot be easily spotted because its cause is so small. So we have to watch our p's and q's very carefully, retaining our ability to look at the big picture while also ensuring that every tiny detail of the picture is correct. This endless attention to detail is not something at which the human brain is very efficient. Surrounded by a large number of potential bugs, we can sometimes struggle to maintain what often feels like a very precarious balance in our programs.

A programmer employed by my company came to me with a bug that he had found impossible to locate. When I looked at the suspect class module, the first thing I noticed was that one of the variables hadn't been declared before being used. Like every conscientious Visual Basic developer, he had set Require Variable Declaration in his Integrated Development Environment (IDE) to warn him about this type of problem. But in a classic case of programming oversight, he had made the perfectly reasonable assumption that setting this option meant that all undeclared variables are always recognized and stomped on. Unfortunately, it applies only to new modules developed from the point at which the flag is set. Any modules written within one developer's IDE and then imported into another programmer's IDE are never checked for undeclared variables unless that first developer also specified Require Variable Declaration. This is obvious when you realize how the option functions. It simply inserts Option Explicit at the top of each module when it is first created. What it doesn't do is act globally on all modules. This point is easy to recognize when you stop and think for a moment, but it's also very easy to miss.

Some final thoughts Learn to be humble when programming. This stuff is seriously nontrivial (a fancy term for swallowing a rhinoceros sideways), and arrogance when you're trying to write stable code is counterproductive.

Jumping Out of The Loop

One psychological factor responsible for producing bugs and preventing their detection is an inability to jump from one mind-set to another.In our push to examine subtle details, we often overlook the obvious. The results of a study performed a decade ago showed that that 50 percent of all errors plainly visible on a screen or report were still overlooked by the programmer. The kind of mistake shown in the preceding sentence ("that" repeated) seems fairly obvious in retrospect, but did you spot it the first time through?

One reason for this tendency to overlook the obvious is that the mind-set required to find gross errors is so different from the mind-set needed to locate subtle errors that it is hard to switch between the two. We've all been in the situation in which the cause of a bug eludes us for hours, but as soon as we explain the problem to another programmer, the cause of the error immediately becomes obvious. Often in this type of confessional programming, the other developer doesn't have to say a word, just nod wisely. The mental switch from an internal monologue to an external one is sometimes all that we need to force us into a different mind-set, and we can then reevaluate our assumptions about what is happening in the code. Like one of those infuriating Magic Eye pictures, the change of focus means that what was hidden before suddenly becomes clear.

Some final thoughts If you're stuck on a particularly nasty bug, try some lateral thinking. Use confessional programming—explain the problem to a colleague. Perhaps take a walk to get some fresh air. Work on something entirely different for a few minutes, returning later with a clearer mind for the problem. Or you can go so far as to picture yourself jumping out of that mental loop, reaching a different level of thought. All of these techniques can help you avoid endlessly traversing the same mental pathways.


Getting Our Hands Dirty

Steve Maguire, in his excellent book Writing Solid Code (Microsoft Press, 1995), stresses that many of the best techniques and tools developed for the eradication of bugs came from programmers asking the following two questions every time a bug is found:

  • How could I have automatically detected this bug?

  • How could I have prevented this bug?

In the following sections, we'll look at some of the bugs Visual Basic 6 programmers are likely to encounter, and I'll suggest, where appropriate, ways of answering both of the above questions. Applying this lesson of abstracting from the specific problem to the general solution can be especially effective when carried out in a corporate environment over a period of time. Given a suitable corporate culture, in which every developer has the opportunity to formulate general answers to specific problems, a cumulative beneficial effect can accrue. The more that reusable code is available to developers, the more it will be utilized. Likewise, the more information about the typical bugs encountered within an organization that is stored and made available in the form of a database, the more likely it is that the programmers with access to that information will search for the information and use it appropriately. In the ideal world, all this information would be contributed both in the form of reusable code and in a database of problems and solutions. Back in the real world, one or the other method may have to suffice.

Some final thoughts Document all system testing, user acceptance testing, and production bugs and their resolution. Make this information available to the developers and testers and their IS managers. Consider using an application's system testing and user acceptance bug levels to determine when that application is suitable for release to the next project phase.

In-Flight Testing: Using the Assert Statement

One of the most powerful debugging tools available, at least to C programmers, is the Assert macro. Simple in concept, it allows a programmer to write self-checking code by providing an easy method of verifying that a particular condition or assumption is true. Visual Basic programmers had no structured way of doing this until Visual Basic 5. Now we can write statements like this:

Debug.Assert 2 + 2 = 4

Debug.Assert bFunctionIsArrayHealthy

Select Case iUserChoice
    Case 1
        DoSomething1
    Case 2
        DoSomething2
    Case Else
        ' We should never reach here!
        Debug.Assert nUserChoice = 1 Or nUserChoice = 2
End Select

Debug.Assert operates in the development environment only—conditional compilation automatically drops it from the compiled EXE. It will take any expression that evaluates to either TRUE or FALSE and then drop into break mode at the point the assertion is made if that expression evaluates to FALSE. The idea is to allow you to catch bugs and other problems early by verifying that your assumptions about your program and its environment are true. You can load your program code with debug checks; in fact, you can create code that checks itself while running. Holes in your algorithms, invalid assumptions, creaky data structures, and invalid procedure arguments can all be found in flight and without any human intervention.

The power of assertions is limited only by your imagination. Suppose you were using Visual Basic 6 to control the space shuttle. (We can dream, can't we?) You might have a procedure that shuts down the shuttle's main engine in the event of an emergency, perhaps preparing to jettison the engine entirely. You would want to ensure that the shutdown had worked before the jettison took place, so the procedure for doing this would need to return some sort of status code. To check that the shutdown procedure was working correctly during debugging, you might want to perform a different version of it as well and then verify that both routines left the main engine in the same state. It is fairly common to code any mission-critical system features in this manner. The results of the two different algorithms can be checked against each other, a practice that would fail only in the relatively unlikely situation of both the algorithms having the same bug. The Visual Basic 6 code for such testing might look something like this:

' Normal shutdown
Set nResultOne = ShutdownTypeOne(objEngineCurrentState)

' Different shutdown
Set nResultTwo = ShutdownTypeTwo(objEngineCurrentState)

' Check that both shutdowns produced the same result.
Debug.Assert nResultOne = nResultTwo

When this code was released into production, you would obviously want to remove everything except the call to the normal shutdown routine and let Visual Basic 6's automatic conditional compilation drop the Debug.Assert statement.

You can also run periodic health checks on the major data structures in your programs, looking for uninitialized or null values, holes in arrays, and other nasty gremlins:

Debug.Assert bIsArrayHealthy CriticalArray

Assertions and Debug.Assert are designed for the development environment only. In the development environment, you are trading program size and speed for debug information. Once your code has reached production, the assumption is that it's been tested well and that assertions are no longer necessary. Assertions are for use during development to help prevent developers from creating bugs. On the other hand, other techniques—such as error handling or defensive programming—attempt to prevent data loss or other undesirable effects as a result of bugs that already exist.

Also, experience shows that a system loaded with assertions can run from 20 to 50 percent slower than one without the assertions, which is obviously not suitable in a production environment. But because the Debug.Assert statements remain in your source code, they will automatically be used again whenever your code is changed and retested in the development environment. In effect, your assertions are immortal—which is as it should be. One of the hardest trails for a maintenance programmer to follow is the one left by your own assumptions about the state of your program. Although we all try to avoid code dependencies and subtle assumptions when we're designing and writing our code, they invariably tend to creep in. Real life demands compromise, and the best-laid code design has to cope with some irregularities and subtleties. Now your assertion statements can act as beacons, showing the people who come after you what you were worried about when you wrote a particular section of code. Doesn't that give you a little frisson?

Another reason why Debug.Assert is an important new tool in your fight against bugs is the inexorable rise of object-oriented programming. A large part of object-oriented programming is what I call "design by contract." This is where you design and implement an object hierarchy in your Visual Basic program, and expose methods and properties of your objects for other developers (or yourself) to use. In effect, you're making a contract with these users of your program. If they invoke your methods and properties correctly, perhaps in a specific order or only under certain conditions, they will receive the services or results that they want. Now you are able to use assertions to ensure that your methods are called in the correct order, perhaps, or that the class initialization method has been invoked before any other method. Whenever you want to confirm that your class object is being used correctly and is in an internally consistent state, you can simply call a method private to that class that can then perform the series of assertions that make up the "health check."

One situation in which to be careful occurs when you're using Debug.Assert to invoke a procedure. You need to bear in mind that any such invocation will never be performed in the compiled version of your program. If you copy the following code into an empty project, you can see clearly what will happen:

Option Explicit
Dim mbIsThisDev As Boolean

Private Sub Form_Load()
mbIsThisDev = False

' If the following line executes, the MsgBox will display
' True in answer to its title "Is this development?"
' If it doesn't execute, the MsgBox will display false.

Debug.Assert SetDevFlagToTrue
MsgBox mbIsThisDev, vbOKOnly, "Is this development?"

Unload Me
End Sub

Private Function SetDevFlagToTrue() As Boolean

SetDevFlagToTrue = True
mbIsThisDev = True

End Function

When you run this code in the Visual Basic environment, the message box will state that it's true that your program is running within the Visual Basic IDE because the SetDevFlagToTrue function will be invoked. If you compile the code into an EXE, however, the message box will show FALSE. In other words, the SetDevFlagToTrue function is not invoked at all. Offhand, I can't think of a more roundabout method of discovering whether you're running as an EXE or in the Visual Basic 6 IDE.

When should you assert?

Once you start using assertions seriously in your code, you need to be aware of some pertinent issues. The first and most important of these is when you should assert. The golden rule is that assertions should not take the place of either defensive programming or data validation. It is important to remember, as I stated earlier, that assertions are there to help prevent developers from creating bugs—an assertion is normally used only to detect an illegal condition that should never happen if your program is working correctly. Defensive programming, on the other hand, attempts to prevent data loss or other undesirable effects as a result of bugs that already exist.

To return to the control software of our space shuttle, consider this code:

Function ChangeEnginePower(ByVal niPercent As Integer) As Integer
Dim lNewEnginePower As Long

Debug.Assert niPercent => -100 And niPercent =< 100
Debug.Assert mnCurrentPower => 0 And mnCurrentPower =< 100

lNewEnginePower = CLng(mnCurrentPower) + niPercent

If lNewEnginePower < 0 Or lNewEnginePower > 100
Err.Raise vbObjectError + mgInvalidEnginePower
Else
mnCurrentPower = lNewEnginePower
End If

ChangeEnginePower = mnCurrentPower

End Sub

Here we want to inform the developer during testing if he or she is attempting to change the engine thrust by an illegal percentage, or if the current engine thrust is illegal. This helps the developer catch bugs during development. However, we also want to program defensively so that if a bug has been created despite our assertion checks during development, it won't cause the engine to explode. The assertion is in addition to some proper argument validation that handles nasty situations such as trying to increase engine thrust beyond 100%. In other words, don't ever let assertions take the place of normal validation.

Defensive programming like the above is dangerous if you don't include the assertion statement. Although using defensive programming to write what might be called nonstop code is important for the prevention of user data loss as a result of program crashes, defensive programming can also have the unfortunate side effect of hiding bugs. Without the assertion statement, a programmer who called the ChangeEnginePower routine with an incorrect argument would not necessarily receive any warning of a problem. Whenever you find yourself programming defensively, think about including an assertion statement.

Explain your assertions

Perhaps the only thing more annoying than finding an assertion statement in another programmer's code and having no idea why it's there is finding a similar assertion statement in your own code. Document your assertions. A simple one- or two-line comment will normally suffice—you don't need to write a dissertation. Some assertions can be the result of quite subtle code dependencies, so in your comment try to clarify why you're asserting something, not just what you're asserting.

Beware of Boolean coercion

The final issue with Debug.Assert is Boolean type coercion. Later in this chapter, we'll look at Visual Basic's automatic type coercion rules and where they can lay nasty traps for you. For now, you can be content with studying the following little enigma:

Dim nTest As Integer
nTest = 50
Debug.Assert nTest
Debug.Assert Not nTest

You will find that neither of these assertions fire! Strange, but true. The reason has to do with Visual Basic coercing the integer to a Boolean. The first assertion says that nTest = 50, which, because nTest is nonzero, is evaluated to TRUE. The second assertion calculates Not nTest to be -51, which is also nonzero and again evaluated to TRUE.

However, if you compare nTest and Not nTest to the actual value of TRUE (which is -1) as in the following code, only the first assertion fires:

Debug.Assert nTest = True
Debug.Assert Not nTest = True

Some final thoughts Debug.Assert is a very powerful tool for bug detection. Used properly, it can catch many bugs automatically, without any human intervention. (See the discussion of an Assertion Sourcerer for a utility that supplements Debug.Assert.) Also see Chapter 1 for further discussion of Debug.Assert.

How Sane Is Your Program?

A source-level debugger such as the one available in Visual Basic is a wonderful tool. It allows you to see into the heart of your program, watching data as it flows through your code. Instead of taking a "black box," putting input into it, and then checking the output and guessing at what actually happened between the two, you get the chance to examine the whole process in detail.

Back in the 1950s, many people were still optimistic about the possibility of creating a machine endowed with human intelligence. In 1950, English mathematician Alan Turing proposed a thought experiment to test whether a machine was intelligent. His idea was that anybody who wanted to verify a computer program's intelligence would be able to interrogate both the program in question and a human being via computer links. If after asking a series of questions, the interrogator was unable to distinguish between the human and the program, the program might legitimately be considered intelligent. This experiment had several drawbacks, the main one being that it is very difficult to devise the right type of questions. The interrogator would forever be devising new questions and wondering about the answers to the current ones.

This question-and-answer process is remarkably similar to what happens during program testing. A tester devises a number of inputs (equivalent to asking a series of questions) and then carefully examines the output (listens to the computer's answers). And like Turing's experiment, this type of black-box testing has the same drawbacks. The tester simply can't be sure whether he or she is asking the right questions or when enough questions have been asked to be reasonably sure that the program is functioning correctly.

What a debugger allows you to do is dive below the surface. No longer do you have to be satisfied with your original questions. You can observe your program's inner workings, redirect your questions in midflight to examine new issues raised by watching the effect of your original questions on the code, and be much more aware of which questions are important. Unlike a psychiatrist, who can never be sure whether a patient is sane, using a source-level debugger means that you will have a much better probability of being able to evaluate the sanity of your program.

Debugging windows

Visual Basic 6's source-level debugger has three debugging windows as part of the IDE.

  • The Immediate (or Debug) window is still here, with all the familiar abilities, such as being able to execute single-line statements or subroutines.

  • The Locals window is rather cool. It displays the name, value, and data type of each variable declared in the current procedure. It can also show properties. You can change the value of any variable or property merely by clicking on it and then typing the new value. This can save a lot of time during debugging.

  • The Watches window also saves you some time, allowing you to watch a variable's value without having to type any statements into the Immediate window. You can easily edit the value of any Watch expression you've set or the Watch expression itself by clicking on it, just as you can in the Locals window.

Debugging hooks

One technique that many programmers have found useful when working with this type of interactive debugger is to build debugging hooks directly into their programs. These hooks, usually in the form of functions or subroutines, can be executed directly from the Immediate window when in break mode. An example might be a routine that walks any array passed to it and prints out its contents, as shown here:

Public Sub DemonstrateDebugHook()
Dim saTestArray(1 to 4) As Integer
saTestArray(1) = "Element one"
saTestArray(2) = "Element two"
saTestArray(3) = "Element three"
saTestArray(4) = "Element four"

Stop

End Sub

Public Sub WalkArray(ByVal vntiArray As Variant)
Dim nLoop As Integer

' Check that we really have an array.
Debug.Assert IsArray(vntiArray)

' Print the array type and number of elements.
Debug.Print "Array is of type " & TypeName(vntiArray)
nLoop = UBound(vntiArray) - LBound(vntiArray) + 1
Debug.Print "Array has " & CStr(nLoop) & " elements""

' Walk the array, and print its elements.
For nLoop = LBound(vntiArray) To UBound(vntiArray)
    Debug.Print "Element " & CStr(nLoop) & " contains:" _
        & vntiArray(nLoop)
Next nLoop

End Sub

When you run this code, Visual Basic will go into break mode when it hits the Stop statement placed in DemonstrateDebugHook. You can then use the Immediate window to type:

WalkArray saTestArray

This debugging hook will execute and show you all the required information about any array passed to it.

NOTE

The array is received as a Variant so that any array type can be handled and the array can be passed by value. Whole arrays can't be passed by value in their natural state. These types of debugging hooks placed in a general debug class or module can be extremely useful, both for you and for any programmers who later have to modify or debug your code.

Exercising all the paths

Another effective way to use the debugger is to step through all new or modified code to exercise all the data paths contained within one or more procedures. You can do this quickly, often in a single test run. Every program has code that gets executed only once in a very light blue moon, usually code that handles special conditions or errors. Being able to reset the debugger to a particular source statement, change some data to force the traversal of another path, and then continue program execution from that point gives you a great deal of testing power.

' This code will work fine - until nDiv is zero.
If nDiv > 0 And nAnyNumber / nDiv > 1 Then
    DoSomething
Else
    DoSomethingElse
End If

When I first stepped through the above code while testing another programmer's work, nDiv had the value of 1. I stepped through to the End If statement—everything looked fine. Then I used the Locals window to edit the nDiv variable and change it to zero, set the debugger to execute the first line again, and of course the program crashed. (Visual Basic doesn't short-circuit this sort of expression evaluation. No matter what the value of nDiv, the second expression on the line will always be evaluated.) This ability to change data values and thereby follow all the code paths through a procedure is invaluable in detecting bugs that might otherwise take a long time to appear.

Peering Inside Stored Procedures

One of the classic bugbears of client/server programming is that it's not possible to debug stored procedures interactively. Instead, you're forced into the traditional edit-compile-test cycle, treating the stored procedure that you're developing as an impenetrable black box. Pass in some inputs, watch the outputs, and try to guess what happened in between. Visual Basic 5 and Visual Basic 6 contain something that's rather useful: a Transact-SQL (T-SQL) interactive debugger.

There are a few constraints. First of all, you must be using the Enterprise Edition of Visual Basic 6. Also, the only supported server-side configuration is Microsoft SQL Server 6.5 or later. Finally, you also need to be running SQL Server Service Pack 3 or later. When installing Visual Basic 6, select Custom from the Setup dialog box, choose Enterprise Tools, and click Select All to ensure that all the necessary client-side components are installed. Once Service Pack 3 is installed, you can install and register the T-SQL Debugger interface and Remote Automation component on the server.

The T-SQL Debugger works through a UserConnection created with Microsoft UserConnection, which is available by selecting the Add Microsoft UserConnection option of the Project menu. Once you've created a UserConnection object, just create a Query object for the T-SQL query you want to debug. This query can be either a user-defined query that you build using something like Microsoft Query, or a stored procedure.

The T-SQL Debugger interface is similar to most language debuggers, allowing you to set breakpoints, change local variables or parameters, watch global variables, and step through the code. You can also view the contents of global temporary tables that your stored procedure creates and dump the resultset of the stored procedure to the output window. If your stored procedure creates multiple resultsets, right-click the mouse button over the output window and select More Results to view the next resultset.

Some final thoughts The combination of these two powerful interactive debuggers, including their new features, makes it even easier to step through every piece of code that you write, as soon as you write it. Such debugging usually doesn't take nearly as long as many developers assume and can be used to promote a much better understanding of the structure and stability of your programs.

NOTE

How low can you get? One of Visual Basic 6's compile options allows you to add debugging data to your native-code EXE. This gives you the ability to use a symbolic debugger, such as the one that comes with Microsoft Visual C++, to debug and analyze your programs at the machine-code level. See Chapter 7 and Chapter 1 (both by Peter Morris) for more information about doing this.

Here Be Dragons

As I told you early in this chapter, a typical medieval map of the world marked unexplored areas with the legend "Here be dragons," often with little pictures of exotic sea monsters. This section of the chapter is the modern equivalent, except that most of these creatures have been studied and this map is (I hope) much more detailed than its medieval counterpart. Now we can plunge into the murky depths of Visual Basic, where we will find a few real surprises.

Bypassing the events from hell

Visual Basic's GotFocus and LostFocus events have always been exasperating to Visual Basic programmers. They don't correspond to the normal KillFocus and SetFocus messages generated by Windows; they don't always execute in the order that you might expect; they are sometimes skipped entirely; and they can prove very troublesome when you use them for field-by-field validation.

Microsoft has left these events alone in Visual Basic 6, probably for backward compatibility reasons. However, the good news is that the technical boys and girls at Redmond do seem to have been hearing our calls for help. Visual Basic 6 gives us the Validate event and CausesValidation property, whose combined use avoids our having to use the GotFocus and LostFocus events for validation, thereby providing a mechanism to bypass all the known problems with these events. Unfortunately, the bad news is that the new mechanism for field validation is not quite complete.

Before we dive into Validate and CausesValidation, let's look at some of the problems with GotFocus and LostFocus to see why these two events should never be used for field validation. The following project contains a single window with two text box controls, an OK command button, and a Cancel command button. (See Figure 6-1.)

Figure 6-1 Simple interface screen hides events from hell

Both command buttons have an accelerator key. Also, the OK button's Default property is set to TRUE (that is, pressing the Enter key will click this button), and the Cancel button's Cancel property is set to TRUE (that is, pressing the Esc key will click this button). The GotFocus and LostFocus events of all four controls contain a Debug.Print statement that will tell you (in the Immediate window) which event has been fired. This way we can easily examine the order in which these events fire and understand some of the difficulties of using them.

When the application's window is initially displayed, focus is set to the first text box. The Immediate window shows the following:

Program initialization
txtBox1 GotFocus

Just tabbing from the first to the second text box shows the following events:

txtBox1 LostFocus
txtBox2 GotFocus

So far, everything is as expected. Now we can add some code to the LostFocus event of txtBox1 to simulate a crude validation of the contents of txtBox1, something like this:

Private Sub txtBox1_LostFocus

Debug.Print "txtBox1_LostFocus"
If Len(txtBox1.Text) > 0 Then
    txtBox1.SetFocus
End If

End Sub

Restarting the application and putting any value into txtBox1 followed by tabbing to txtBox2 again shows what looks like a perfectly normal event stream:

txtBox1_LostFocus
txtBox2_GotFocus
txtBox2_LostFocus
txtBox1 GotFocus

Normally, however, we want to inform the user if a window control contains anything invalid. So in our blissful ignorance, we add a MsgBox statement to the LostFocus event of txtBox1 to inform the user if something's wrong:

Private Sub txtBox1_LostFocus

Debug.Print "txtBox1_LostFocus"
If Len(txtBox1.Text) > 0 Then
    MsgBox "txtBox1 must not be empty!"
    txtBox1.SetFocus
End If

End Sub

Restarting the application and putting any value into txtBox1 followed by tabbing to txtBox2 shows the first strangeness. We can see that after the message box is displayed, txtBox2 never receives focus—but it does lose focus!

txtBox1_LostFocus
txtBox2_LostFocus
txtBox1 GotFocus

Now we can go further to investigate what happens when both text boxes happen to have invalid values. So we add the following code to the LostFocus event of txtBox2:

Private Sub txtBox2_LostFocus

Debug.Print "txtBox2_LostFocus"
If Len(txtBox2.Text) = 0 Then
    MsgBox "txtBox2 must not be empty!"
    txtBox2.SetFocus
End If

End Sub

Restarting the application and putting any value into txtBox1 followed by tabbing to txtBox2 leads to a program lockup! Because both text boxes contain what are considered to be invalid values, we see no GotFocus events but rather a continuous cascade of LostFocus events as each text box tries to claim focus in order to allow the user to change its invalid contents. This problem is well known in Visual Basic, and a programmer usually gets caught by it only once before mending his or her ways.

At this point, completely removing the MsgBox statements only makes the situation worse. If you do try this, your program goes seriously sleepy-bye-bye. Because the MsgBox function no longer intervenes to give you some semblance of control over the event cascade, you're completely stuck. Whereas previously you could get access to the Task Manager to kill the hung process, you will now have to log out of Windows to regain control.

These are not the only peculiarities associated with these events. If we remove the validation code to prevent the application from hanging, we can look at the event stream when using the command buttons. Restart the application, and click the OK button. The Immediate window shows a normal event stream. Now do this again, but press Enter to trigger the OK button rather than clicking on it. The Debug window shows quite clearly that the LostFocus event of txtBox1 is never triggered. Exactly the same thing happens if you use the OK button's accelerator key (Alt+O)—no LostFocus event is triggered. Although in the real world you might not be too worried if the Cancel button swallows a control's LostFocus event, it's a bit more serious when you want validation to occur when the user presses OK.

The good news with Visual Basic 6 is that you now have a much better mechanism for this type of field validation. Many controls now have a Validate event. The Validate event fires before the focus shifts to another control that has its CausesValidation property set to True. Because this event fires before the focus shifts and also allows you to keep focus on any control with invalid data, most of the problems discussed above go away. In addition, the CausesValidation property means that you have the flexibility of deciding exactly when you want to perform field validation. For instance, in the above project, you would set the OK button's CausesValidation property to True, but the Cancel button's CausesValidation property to False. Why do any validation at all if the user wants to cancel the operation? In my opinion, this is a major step forward in helping with data validation.

Note that I stated that "most" of the problems go away. Unfortunately, "most" is not quite "all." If we add Debug.Print code to the Validate and Click events in the above project, we can still see something strange. Restart the application, and with the focus on the first text box, click on the OK button to reveal a normal event stream:

txtBox1 Validate
txtBox1 LostFocus
cmdOK GotFocus
cmdOK Click

Once again restart the application, and again with the focus on the first text box, press the accelerator key of the OK button to reveal something strange:

txtBox1 Validate
cmdOK Click
txtBox1 LostFocus
cmdOK GotFocus

Hmmm. The OK button's Click event appears to have moved in the event stream from fourth to second. From the data validation point of view, this might not be worrisome. The Validate event still occurs first, and if you actually set the Validate event's KeepFocus argument to True (indicating that txtBox1.Text is invalid), the rest of the events are not executed—just as you would expect.

Once again, restart the application and again with the focus on the first text box, press the Enter key. Because the Default property of the OK button is set to True, this has the effect of clicking the OK button:

cmdOK Click

Oops! No Validate event, no LostFocus or GotFocus events. Pressing the Escape key to invoke the Cancel button has exactly the same effect. In essence, these two shortcut keys bypass the Validate/CausesValidation mechanism completely. If you don't use these shortcut keys, everything is fine. If you do use them, you need to do something such as firing each control's Validate event manually if your user utilizes one of these shortcuts.

Some final thoughts Never rely on GotFocus and LostFocus events actually occurring—or occurring in the order you expect. Particularly, do not use these events for field-by-field validation—use Validate and CausesValidation instead. Note that the Validate event is also available for UserControls.

Evil Type Coercion

A programmer on my team had a surprise when writing Visual Basic code to extract information from a SQL Server database. Having retrieved a recordset, he wrote the following code:

Dim vntFirstValue As Variant, vntSecondValue As Variant
Dim nResultValue1 As Integer, nResultValue2 As Integer

vntFirstValue = Trim(rsMyRecordset!first_value)
vntSecondValue = Trim(rsMyRecordset!second_value)

nResultValue1 = vntFirstValue + vntSecondValue
nResultValue2 = vntFirstValue + vntSecondValue + 1

He was rather upset when he found that the "+" operator not only concatenated the two variants but also added the final numeric value. If vntFirstValue contained "1" and vntSecondValue contained "2," nResultValue1 had the value 12 and nResultValue2 had the value 13.

To understand exactly what's going on here, we have to look at how Visual Basic handles type coercion. Up until Visual Basic 3, type coercion was relatively rare. Although you could write Visual Basic 3 code like this:

txtBox.Text = 20

and find that it worked without giving any error, almost every other type of conversion had to be done explicitly by using statements such as CStr and CInt. Starting with Visual Basic 4, and continuing in Visual Basic 5 and 6, performance reasons dictated that automatic type coercion be introduced. Visual Basic no longer has to convert an assigned value to a Variant and then unpack it back into whatever data type is receiving the assignment. It can instead invoke a set of hard-coded coercion rules to perform direct coercion without ever involving the overhead of a Variant. Although this is often convenient and also achieves the laudable aim of good performance, it can result in some rather unexpected results. Consider the following code:

Sub Test()

    Dim sString As String, nInteger As Integer
    sString = "1"
    nInteger = 2
    ArgTest sString, nInteger

End Sub

Sub ArgTest(ByVal inArgument1 As Integer, _
            ByVal isArgument2 As String)
    ' Some code here
End Sub

In Visual Basic 3, this code would give you an immediate error at compile time because the arguments are in the wrong order. In Visual Basic 4 or later, you won't get any error because Visual Basic will attempt to coerce the string variable into the integer parameter and vice versa. This is not a pleasant change. If inArgument1 is passed a numeric value, everything looks and performs as expected. As soon as a non-numeric value or a null string is passed, however, a run-time error occurs. This means that the detection of certain classes of bugs has been moved from compile time to run time, which is definitely not a major contribution to road safety.

The following table shows Visual Basic 6's automatic type coercion rules.
Source Type | Coerced To | Apply This Rule
Integer | Boolean | 0=False, nonzero=True
Boolean | Byte | False=0, True=-1 (except Byte
Boolean | Any numeric | False=0, True=-1 (except Byte
String | Date | String is analyzed for MM/dd/yy and so on
Date | Numeric type | Coerce to Double and useDateSerial(Double)
Numeric | Date | Use number as serial date, check valid date range
Numeric | Byte | Error if negative
String | Numeric type | Treat as Double when representing a number

Some final thoughts Any Visual Basic developer with aspirations to competence should learn the automatic type coercion rules and understand the most common situations in which type coercion's bite can be dangerous.

Arguing safely

In Visual Basic 3, passing arguments was relatively easy to understand. You passed an argument either by value (ByVal) or by reference (ByRef). Passing ByVal was safer because the argument consisted only of its value, not of the argument itself. Therefore, any change to that argument would have no effect outside the procedure receiving the argument. Passing ByRef meant that a direct reference to the argument was passed. This allowed you to change the argument if you needed to do so.

With the introduction of objects, the picture has become more complicated. The meaning of ByVal and ByRef when passing an object variable is slightly different than when passing a nonobject variable. Passing an object variable ByVal means that the type of object that the object variable refers to cannot change. The object that the object variable refers to is allowed to change, however, as long as it remains the same type as the original object. This rule can confuse some programmers when they first encounter it and can be a source of bugs if certain invalid assumptions are made.

Type coercion introduces another wrinkle to passing arguments. The use of ByVal has become more dangerous because Visual Basic will no longer trigger certain compile-time errors. In Visual Basic 3, you could never pass arguments to a procedure that expected arguments of a different type. Using ByVal in Visual Basic 6 means that an attempt will be made to coerce each ByVal argument into the argument type expected. For example, passing a string variable ByVal into a numeric argument type will not show any problem unless the string variable actually contains non-numeric data at run time. This means that this error check has to be delayed until run time—see the earlier section called "Evil Type Coercion" for an example and more details.

If you don't specify an argument method, the default is that arguments are passed ByRef. Indeed, many Visual Basic programmers use the language for a while before they realize they are using the default ByRef and that ByVal is often the better argument method. For the sake of clarity, I suggest defining the method being used every time rather than relying on the default. I'm also a firm believer in being very precise about exactly which arguments are being used for input, which for output, and which for both input and output. A good naming scheme should do something like prefix every input argument with "i" and every output argument with "o" and then perhaps use the more ugly "io" to discourage programmers from using arguments for both input and output. Input arguments should be passed ByVal, whereas all other arguments obviously have to be passed ByRef. Being precise about the nature and use of procedure arguments can make the maintenance programmer's job much easier. It can even make your job easier by forcing you to think clearly about the exact purpose of each argument.

One problem you might run into when converting from previous versions of Visual Basic to Visual Basic 6 is that you are no longer allowed to pass a control to a DLL or OCX using ByRef. Previously, you might have written your function declaration like this:

Declare Function CheckControlStatus Lib "MY.OCX" _
          (ctlMyControl As Control) As Integer

You are now required to specify ByVal rather than the default ByRef. Your function declaration must look like this:

Declare Function CheckControlStatus Lib "MY.OCX" _
          (ByVal ctlMyControl As Control) As Integer

This change is necessary because DLL functions now expect to receive the Windows handle of any control passed as a parameter. Omitting ByVal causes a pointer to the control handle to be passed rather than the control handle itself, which will result in undefined behavior and possibly a GPF.

The meaning of zero

Null, IsNull, Nothing, vbNullString, "", vbNullChar, vbNull, Empty, vbEmpty… Visual Basic 6 has enough representations of nothing and zero to confuse the most careful programmer. To prevent bugs, programmers must understand what each of these Visual Basic keywords represents and how to use each in its proper context. Let's start with the interesting stuff.

Private sNotInitString As String
Private sEmptyString As String
Private sNullString As String
sEmptyString = ""
sNullString = 0&

Looking at the three variable declarations above, a couple of questions spring to mind. What are the differences between sNotInitString, sEmptyString, and sNullString? When is it appropriate to use each declaration, and when is it dangerous? The answers to these questions are not simple, and we need to delve into the murky depths of Visual Basic's internal string representation system to understand the answers.

After some research and experimentation, the answer to the first question becomes clear but at first sight is not very illuminating. The variable sNotInitString is a null pointer string, held internally as a pointer that doesn't point to any memory location and that holds an internal value of 0. sEmptyString is a pointer to an empty string, a pointer that does point to a valid memory location. Finally, sNullString is neither a null string pointer nor an empty string but is just a string containing 0.

Why does sNotInitString contain the internal value 0? In earlier versions of Visual Basic, uninitialized variable-length strings were set internally to an empty string. Ever since the release of Visual Basic 4, however, all variables have been set to 0 internally until initialized. Developers don't normally notice the difference because, inside Visual Basic, this initial zero value of uninitialized strings always behaves as if it were an empty string. It's only when you go outside Visual Basic and start using the Windows APIs that you receive a shock. Try passing either sNotInitString or sEmptyString to any Windows API function that takes a null pointer. Passing sNotInitString will work fine because it really is a null pointer, whereas passing sEmptyString will cause the function to fail. Of such apparently trivial differences are the really nasty bugs created.

The following code snippet demonstrates what can happen if you're not careful.

Private Declare Function WinFindWindow Lib "user32" Alias _
    "FindWindowA" (ByVal lpClassName As Any, _
                   ByVal lpWindowName As Any) As Long

    Dim sNotInitString As String
    Dim sEmptyString As String
    Dim sNullString As String

    sEmptyString = ""
    sNullString = 0&

    Shell "Calc.exe", 1
    DoEvents
    ' This will work.
    x& = WinFindWindow(sNotInitString, "Calculator")

    ' This won't work.
    x& = WinFindWindow(sEmptyString, "Calculator")

    ' This will work.
    x& = WinFindWindow(sNullString, "Calculator")

Now that we've understood one nasty trap and why it occurs, the difference between the next two variable assignments becomes clearer.

sNullPointer = vbNullString
sEmptyString = ""

It's a good idea to use the former assignment rather than the latter, for two reasons. The first reason is safety. Assigning sNullPointer as shown here is the equivalent of sNotInitString in the above example. In other words, it can be passed to a DLL argument directly. However, sEmptyString must be assigned the value of 0& before it can be used safely in the same way. The second reason is economy. Using "" will result in lots of empty strings being scattered throughout your program, whereas using the built-in Visual Basic constant vbNullString will mean no superfluous use of memory.

Null and IsNull are fairly clear. Null is a variant of type vbNull that means no valid data and typically indicates a database field with no value. The only hazard here is a temptation to compare something with Null directly, because Null will propagate through any expression that you use. Resist the temptation and use IsNull instead.

' This will always be false.
If sString = Null Then
    ' Some code here
End If

Continuing through Visual Basic 6's representations of nothing, vbNullChar is the next stop on our travels. This constant is relatively benign, simply CHR$(0). When you receive a string back from a Windows API function, it is normally null-terminated because that is the way the C language expects strings to look. Searching for vbNullChar is one way of determining the real length of the string. Beware of using any API string without doing this first, because null-terminated strings can cause some unexpected results in Visual Basic, especially when displayed or concatenated together.

Finally, two constants are built into Visual Basic for use with the VarType function. vbNull is a value returned by the VarType function for a variable that contains no valid data. vbEmpty is returned by VarType for a variable that is uninitialized. Better people than I have argued that calling these two constants vbTypeNull and vbTypeEmpty would better describe their correct purpose. The important point from the perspective of safety is that vbEmpty can be very useful for performing such tasks as ensuring that the properties of your classes have been initialized properly.

The Bug Hunt

Two very reliable methods of finding new bugs in your application are available. The first involves demonstrating the program, preferably to your boss. Almost without exception, something strange and/or unexpected will happen, often resulting in severe embarrassment. Although this phenomenon has no scientific explanation, it's been shown to happen far too often to be merely a chance occurrence.

The other guaranteed way of locating bugs is to release your application into production. Out there in a hostile world, surrounded by other unruly applications and subject to the vagaries of exotic hardware devices and unusual Registry settings, it's perhaps of little surprise that the production environment can find even the most subtle of weaknesses in your program. Then there are your users, many of whom will gleefully inform you that "your program crashed" without even attempting to explain the circumstances leading up to the crash. Trying to extract the details from them is at best infuriating, at worst impossible. So we need some simple method of trapping all possible errors and logging them in such a way as to be able to reconstruct the user's problem. Here we'll examine the minimum requirements needed to trap and report errors and thus help your user retain some control over what happens to his or her data after a program crash.

The first point to note about Visual Basic 6's error handling capabilities is that they are somewhat deficient when compared with those of most compiled languages. There is no structured exception handling, and the only way to guarantee a chance of recovery from an error is to place an error trap and an error handler into every procedure. To understand why, we need to look in detail at what happens when a run-time error occurs in your program.

Your program is riding happily down the information highway, and suddenly it hits a large pothole in the shape of a run-time error. Perhaps your user forgot to put a disk into drive A, or maybe the Windows Registry became corrupted. In other words, something fairly common happened. Visual Basic 6 first checks whether you have an error trap enabled in the offending procedure. If it finds one, it will branch to the enabled error handler. If not, it will search backward through the current procedure call stack looking for the first error trap it can locate. If none are found, your program will terminate abruptly with a rude error message, which is normally the last thing you want to happen. Losing a user's data in this manner is a fairly heinous crime and is not likely to endear you to either your users or the technical support people. So at the very least you need to place an error handler in the initial procedure of your program.

Unfortunately, this solution is not very satisfactory either, for two reasons. Another programmer could come along later and modify your code, inserting his or her own local error trap somewhere lower in the call stack. This means that the run-time error could be intercepted, and your "global" error trap might never get the chance to deal with it properly. Instead, your program has to be happy with some fly-by-night error handler dealing with what could be a very serious error. The other problem is that even if, through good luck, your global error trap receives the error, Visual Basic 6 provides no mechanism for retrying or bypassing an erroneous statement in a different procedure. So if the error was something as simple as being unable to locate a floppy disk, you're going to look a little silly when your program can't recover. The only way of giving your user a chance of getting around a problem is to handle it in the same procedure in which it occurred.

There is no getting away from the fact that you need to place an error trap and an error handler in every single procedure if you want to be able to respond to and recover from errors in a sensible way. The task then is to provide a minimalist method of protecting every procedure while dealing with all errors in a centralized routine. That routine must be clever enough to discriminate between the different types of errors, log each error, interrogate the user (if necessary) about which action to take, and then return control back to the procedure where the problem occurred. The other minimum requirement is to be able to raise errors correctly to your clients when you are writing ActiveX components.

Adding the following code to every procedure in your program is a good start:

Private Function AnyFunction() As Integer

On Error GoTo LocalError
    ' Normal procedure code goes here.

    Exit Function
LocalError:
    If Fatal("Module.AnyFunction") = vbRetry Then
        Resume
    Else
        Resume Next
    End If

End Function

This code can provide your program with comprehensive error handling, as long as the Fatal function is written correctly. Fatal will receive the names of the module and procedure where the error occurred, log these and other error details to a disk log file for later analysis, and then inform the program's operator about the error and ask whether it ought to retry the statement in error, ignore it, or abort the whole program. If the user chooses to abort, the Fatal function needs to perform a general cleanup and then shutdown the program. If the user makes any other choice, the Fatal function returns control back to the procedure in error, communicating what the user has chosen. The code needed for the Fatal function can be a little tricky. You need to think about the different types of error that can occur, including those raised by ActiveX components. You also need to think about what happens if an error ever occurs within the Fatal function itself. (Again, see Chapter 1 for a more detailed analysis of this type of error handling.) Here I'll examine a couple of pitfalls that can occur when handling or raising Visual Basic 6 errors that involve the use of vbObjectError.

When creating an ActiveX component, you often need to either propagate errors specific to the component back to the client application or otherwise deal with an error that occurs within the component. One accepted method for propagating errors is to use Error.Raise. To avoid clashes with Visual Basic 6's own range of errors, add your error number to the vbObjectError constant. Don't raise any errors within the range vbObjectError through vbObjectError + 512, as Visual Basic 6 remaps some error messages between vbObjectError and vbObjectError + 512 to standard Automation run-time errors. User-defined errors should therefore always be in the range vbObjectError + 512 to vbObjectError + 65536. Note that if you're writing a component that in turn uses other components, it is best to remap any errors raised by these subcomponents to your own errors. Developers using your component will normally want to deal only with the methods, properties, and errors that you define, rather than being forced to deal with errors raised directly by subcomponents.

When using a universal error handler to deal with many different types of problems, always bear in mind that you might be receiving errors that have been raised using the constant vbObjectError. You can use the And operator (Err.Number And vbObjectError) to check this. If True is returned, you should subtract vbObjectError from the actual error number before displaying or logging the error. Because vbObjectError is mainly used internally for interclass communications, there is seldom any reason to display it in its natural state.

In any error handler that you write, make sure that the first thing it does is to save the complete error context, which is all the properties of the Err object. Otherwise it's all too easy to lose the error information. In the following example, if the Terminate event of MyObject has an On Error statement (as it must if it's to handle any error without terminating the program), the original error context will be lost and the subsequent Err.Raise statement will itself generate an "Illegal function call" error. Why? Because you're not allowed to raise error 0!

Private Sub AnySub()
On Error GoTo LocalError
' Normal code goes here

Exit Sub
LocalError:
    Set MyObject = Nothing    ' Invokes MyObject's Terminate event
    Err.Raise Err.Number, , Err.Description
End Sub

Another point to be careful about is raising an error in a component that might become part of a Microsoft Transaction Server (MTS) package. Any error raised by an MTS object to a client that is outside MTS causes a rollback of any work done within that process. This is the so-called "failfast" policy, designed to prevent erroneous data from being committed or distributed. Instead of raising an error, you will have to return errors using the Windows API approach, in which a function returns an error code rather than raising an error.

A final warning for you: never use the Win32 API function GetLastError to determine the error behind a zero returned from a call to a Win32 API function. A call to this function isn't guaranteed to be the next statement executed. Use instead the Err.LastDLLErr property to retrieve the error details.

Staying compatible

An innocuous set of radio buttons on the Component tab of the Project Properties dialog box allows you to control possibly one of the most important aspects of any component that you write—public interfaces. The Visual Basic documentation goes into adequate, sometimes gory, detail about how to deal with public interfaces and what happens if you do it wrong, but they can be a rather confusing area and the source of many defects.

When you compile your Visual Basic 6 component, the following Globally Unique Identifiers (GUIDs) are created:

  • An ID for the type library

  • A CLSID (class ID) for each creatable class in the type library

  • An IID (interface ID) for the default interface of each Public class in the type library, and also one for the outgoing interface (if the class raises events)

  • A MID (member ID) for each property, method, and event of each class

When a developer compiles a program that uses your component, the class IDs and interface IDs of any objects the program creates are included in the executable. The program uses the class ID to request that your component create an object, and then queries the object for the interface ID. How Visual Basic generates these GUIDs depends on the setting of the aforementioned radio buttons.

The simplest setting is No Compatibility. Each time you compile the component, new class and interface IDs are generated. There is no relation between versions of the component, and programs compiled to use one version of the component cannot use later versions. This means that any time you test your component, you will need to close and reopen your test (client) program in order for it to pick up the latest GUIDs of your component. Failing to do this will result in the infamous error message "Connection to type library or object library for remote process has been lost. Press OK for dialog to remove reference."

The next setting is Project Compatibility. In Visual Basic 5, this setting kept the type library ID constant from version to version, although all the other IDs could vary randomly. This behavior has changed in Visual Basic 6, with class IDs now also constant regardless of version. This change will help significantly with your component testing, although you might still occasionally experience the error mentioned above. If you're debugging an out-of-process component, or an in-process component in a separate instance of Visual Basic, this error typically appears if the component project is still in design mode. Running the component, and then running the test program, should eliminate the problem. If you are definitely already running the component, you might have manually switched the setting from No Compatibility to Project Compatibility. This changes the component's type library ID, so you'll need to clear the missing reference to your component from the References dialog box, then open the References dialog box again and recheck your component.

It is a good idea to create a "compatibility" file as early as possible. This is done by making a compiled version of your component and pointing the Project Compatibility dialog box at this executable. Visual Basic will then use this executable file to maintain its knowledge about the component's GUIDs from version to version, thus preventing the referencing problem mentioned above.

Binary Compatibility is the setting to use if you're developing an enhanced version of an existing component. Visual Basic will then give you dire warnings if you change your interface in such a way as to make it potentially incompatible with existing clients that use your component. Ignore these warnings at your peril! You can expect memory corruptions and other wonderful creatures if you blithely carry on. Visual Basic will not normally complain if, say, you add a new method to your interface, but adding an argument to a current method will obviously invalidate any client program that expects the method to remain unchanged.

Declaring Your Intentions

The answer to the next question might well depend on whether your primary language is Basic, Pascal, or C. What will be the data type of the variable tmpVarB in each of the following declarations?

Dim tmpVarB, tmpVarA As Integer
Dim tmpVarA As Integer, tmpVarB

The first declaration, if translated into C, would produce a data type of integer for tmpVarB. The second declaration, if translated into Pascal, would also produce a data type of integer for tmpVarB. Of course in Visual Basic, either declaration would produce a data type of Variant, which is the default data type if none is explicitly assigned. While this is obvious to an experienced Visual Basic developer, it can catch developers by surprise if they're accustomed to other languages.

Another declaration surprise for the unwary concerns the use of the ReDim statement. If you mistype the name of the array that you are attempting to redim, you will not get any warning, even if you have Option Explicit at the top of the relevant module or class. Instead you will get a totally new array, with the ReDim statement acting as a declarative statement. In addition, if another variable with the same name is created later, even in a wider scope, ReDim will refer to the later variable and won't necessarily cause a compilation error, even if Option Explicit is in effect.

Born again

When declaring a new object, you can use either of the following methods:

' Safer method
Dim wgtMyWidget As Widget
Set wgtMyWidget = New Widget

' Not so safe method
Dim wgtMyWidget As New Widget

The second method of declaring objects is less safe because it reduces your control over the object's lifetime. Because declaring the object as New tells Visual Basic that any time you access that variable it should create a new object if one does not exist, any reference to wgtMyWidget after it has been destroyed will cause it to respawn.

' Not so safe method
Dim wgtMyWidget As New Widget
wgtMyWidget.Name = "My widget"
Set wgtMyWidget = Nothing
If wgtMyWidget Is Nothing Then
    Debug.Print "My widget doesn't exist"
Else
    Debug.Print My widget exists"
End If

In the situation above, wgtMyWidget will always exist. Any reference to wgtMyWidget will cause it to be born again if it doesn't currently exist. Even comparing the object to nothing is enough to cause a spontaneous regeneration. This means that your control over the object's lifetime is diminished, a bad situation in principle.

Safe global variables

In the restricted and hermetically sealed world of discrete components, most developers dislike global variables. The major problem is that global variables break the valuable principle of loose coupling, in which you design each of your components to be reused by itself, with no supporting infrastructure that needs to be re-rigged before reuse is a possibility. If a component you write depends upon some global variables, you are forced to rethink the context and use of these global variables every time you reuse the component. From bitter experience, most developers have found that using global variables is much riskier than using local variables, whose scope is more limited and more controllable.

You should definitely avoid making the code in your classes dependent on global data. Many instances of a class can exist simultaneously, and all of these objects share the global data in your program. Using global variables in class module code also violates the object-oriented programming concept of encapsulation, because objects created from such a class do not contain all of their data.

Another problem with global data is the increasing use of multithreading in Visual Basic to perform faster overall processing of a group of tasks that are liable to vary significantly in their individual execution time. For instance, if you write a multithreaded in-process component (such as a DLL or OCX) that provides objects, these objects are created on client threads; your component doesn't create threads of its own. All of the objects that your component supplies for a specific client thread will reside in the same "apartment" and share the same global data. However, any new client thread will have its own global data in its own apartment, completely separate from the other threads. Sub Main will execute once for each thread, and your component classes or controls that run on the different threads will not have access to the same global data. This includes global data such as the App object.

Other global data issues arise with the use of MTS with Visual Basic. MTS relies heavily on stateless components in order to improve its pooling and allocation abilities. Global and module-level data mean that an object has to be stateful (that is, keep track of its state between method invocations), so any global or module-level variables hinder an object's pooling and reuse by MTS.

However, there might be occasions when you want a single data item to be shared globally by all the components in your program, or by all the objects created from a class module. (The data created in this latter occasion is sometimes referred to as static class data.) One useful means of accomplishing this sharing is to use locking to control access to your global data. Similar to concurrency control in a multiuser database environment, a locking scheme for global data needs a way of checking out a global variable before it's used or updated, and then checking it back in after use. If any other part of the program attempts to use this global variable while it's checked out, an assertion (using Debug.Assert) will trap the problem and signal the potential bug.

One method of implementing this locking would be to create a standard (non-class) module that contains all of your global data. A little-known fact is that you can use properties Get/Let/Set even in standard modules, so you can implement all your global data as private properties of a standard module. Being a standard module, these variables will exist only once and persist for the lifetime of the program. Since the variables are actually declared as private, you can use a locking scheme to control access to them. For example, the code that controls access to a global string variable might look something like this:

'Note that this "public" variable is declared Private
'and is declared in a standard (non-class) module.
Private gsMyAppName As String
Private mbMyAppNameLocked As Boolean
Private mnMyAppNameLockId As Integer

Public Function MyAppNameCheckOut() As Integer
'Check-out the public variable when you start using it.
'Returns LockId if successful, otherwise returns zero.

    Debug.Assert mbMyAppNameLocked = False
    If mbMyAppNameLocked = False Then
        mbMyAppNameLocked = True
        mnMyAppNameLockId = mnMyAppNameLockId + 1
        MyAppNameCheckOut = mnMyAppNameLockId
    Else
        'You might want to raise an error here too,
        'to avoid the programmer overlooking the return code.
        MyAppNameCheckOut = 0
    End If

End Function

Property Get MyAppName(ByVal niLockId As Integer) As String
'Property returning the application name.
'Assert that lock id > 0, just in case nobody's calling CheckOut!
'Assert that lock ids agree, but in production will proceed anyway.
'If lock ids don't agree, you might want to raise an error.

    Debug.Assert niLockId > 0
    Debug.Assert niLockId = mnMyAppNameLockId
    MyAppName = gsMyAppName

End Property

Property Let MyAppName(ByVal niLockId As String, ByVal siNewValue As Integer)
'Property setting the application name.
'Assert that lock id > 0, just in case nobody's calling CheckOut!
'Assert that lock ids agree, but in production will proceed anyway.
'If lock ids don't agree, you might want to raise an error.

    Debug.Assert niLockId > 0
    Debug.Assert niLockId = mnMyAppNameLockId
    gsMyAppName = siNewValue

End Property

Public Function MyAppNameCheckIn() As Boolean
'Check-in the public variable when you finish using it
'Returns True if successful, otherwise returns False

    Debug.Assert mbMyAppNameLocked = True
    If mbMyAppNameLocked = True Then
        mbMyAppNameLocked = False
        MyAppNameCheckIn = True
    Else
        MyAppNameCheckIn = False
    End If

End Function

The simple idea behind these routines is that each item of global data has a current LockId, and you cannot use or change this piece of data without the current LockId. To use a global variable, you first need to call its CheckOut function to get the current LockId. This function checks that the variable is not already checked out by some other part of the program and returns a LockId of zero if it's already being used. Providing you receive a valid (non-zero) LockId, you can use it to read or change the global variable. When you've finished with the global variable, you need to call its CheckIn function before any other part of your program will be allowed to use it. Some code using this global string would look something like this:

Dim nLockId As Integer

nLockId = GlobalData.MyAppNameCheckout
If nLockId > 0 Then
    GlobalData.MyAppName(nLockId) = "New app name"
    Call GlobalData.MyAppNameCheckIn
Else
    'Oops! Somebody else is using this global variable
End If

This kind of locking scheme, in which public data is actually created as private data but with eternal persistence, prevents nearly all the problems mentioned above that are normally associated with global data. If you do want to use this type of scheme, you might want to think about grouping your global routines into different standard modules, depending on their type and use. If you throw all of your global data into one huge pile, you'll avoid the problems of global data, but miss out on some of the advantages of information hiding and abstract data types.

ActiveX Documents

ActiveX documents are an important part of Microsoft's component strategy. The ability to create a Visual Basic 6 application that can be run inside Microsoft Internet Explorer is theoretically very powerful, especially over an intranet where control over the browser being used is possible. Whether this potential will actually be realized is debatable, but Microsoft is certainly putting considerable effort into developing and promoting this technology.

There are some common pitfalls that you need to be aware of, especially when testing the downloading of your ActiveX document into Internet Explorer. One of the first pitfalls comes when you attempt to create a clean machine for testing purposes. If you try to delete or rename the Visual Basic 6 run-time library (MSVBVM60.DLL), you might see errors. These errors usually occur because the file is in use; you cannot delete the run time while Visual Basic is running or if the browser is viewing an ActiveX document. Try closing Visual Basic and/or your browser. Another version of this error is "An error has occurred copying Msvbvm60.dll. Ensure the location specified below is correct:". This error generally happens when there is insufficient disk space on the machine to which you are trying to download. Another pitfall pertaining to MSVBVM60.DLL that you might encounter is receiving the prompt "Opening file DocumentName.VBD. What would you like to do with this file? Open it or save it to disk?" This happens if the Visual Basic run time is not installed, typically because the safety level in Internet Explorer is set to High. You should set this level to Medium or None instead, though I would not advise the latter setting.

The error "The Dynamic Link Library could not be found in the specified path" typically occurs when the ActiveX document that you have been trying to download already exists on the machine. Another error message is "Internet Explorer is opening file of unknown type: DocumentName.VBD from…". This error can be caused by one of several nasties. First make sure that you are using the .vbd file provided by the Package and Deployment Wizard. Then check that the CLSIDs of your .vbd and .exe files are synchronized. To preserve CLSIDs across builds in your projects, select Binary Compatibility on the Components tab of the Project Properties dialog box. Next, make sure that your actxprxy.dll file exists and is registered. Also if your ActiveX document is not signed or safe for scripting, you will need to set the browser safety level to Medium. Incidentally, if you erroneously attempt to distribute Visual Basic's core-dependent .cab files, they won't install using a browser safety level of High, since they are not signed either. Finally, do a run-time error check on your ActiveX document, as this error can be caused by errors in the document's initialization code, particularly in the Initialize or InitProperties procedures.


Some Visual Basic 6 Tools

We now turn to a discussion of the three Sourcerers mentioned at the beginning of the chapter. These tools—the Assertion Sourcerer, the Metrics Sourcerer, and the Instrumentation Sourcerer—will help you detect and prevent bugs in the programs you write.

Registering the Three Sourcerers

All three Sourcerers we're going to discuss are available in the CHAP06 folder on the companion CD. These Sourcerers are designed as Visual Basic 6 add-ins, running as ActiveX DLLs. To register each Sourcerer in the Microsoft Windows 95/98 or the Microsoft Windows NT system Registry, load each project in turn into the Visual Basic 6 IDE and compile it. One more step is required to use the add-ins: you must inform Visual Basic 6 itself about each add-in. This is done by creating an entry in VBAddin.INI under a section named [Add-Ins32]. This entry takes the form of the project connection class name, for example, VB6Assert.Connect=0 for the Assertion Sourcerer. To perform this automatically for all three Sourcerers, just load, compile, and run the BootStrap project available in the CHAP06 folder on the companion CD. This will add the correct entries in the VBAddin.INI file.

Assert Yourself: The Assertion Sourcerer

Although Debug.Assert fulfills its purpose very well, improvements to it would certainly be welcome. It would be nice if you had the ability to report assertion failures in compiled code as well as source code. Because one of the aims of the Enterprise Edition of Visual Basic 6 is to allow components to be built and then distributed across a network, it is quite likely that others in your organization will want to reference the in-process or out-of-process ActiveX servers that you have built using Visual Basic. Ensuring that assertion failures in compiled Visual Basic 6 programs were reported would be a very useful feature, enabling better testing of shared code libraries and allowing the capture of assertion failures during user acceptance testing. This kind of functionality cannot be implemented using Debug.Assert because these statements are dropped from your compiled program. Additionally, because you cannot drop from object code into Visual Basic's debugger on an assertion failure, you are faced with finding some alternative method of reporting the assertion failures.

Step forward the Assertion Sourcerer. This add-in supplements Debug.Assert with the functionality mentioned above. When you have registered the Sourcerer in the Registry and used the Add-In Manager to reference it, you can select Assertion Sourcerer from the Add-Ins menu to see the window shown in Figure 6-2.

Figure 6-2 The Assertion Sourcerer dialog box

The standard assertion procedure, which supplements the Debug.Assert functionality, is named BugAssert. It is part of a small Visual Basic 6 module named DEBUG.BAS, which you should add to any project in which you want to monitor run-time assertion failures. You can then specify which of your Debug.Assert statements you want converted to run-time assertions; the choices are all assertions in the project or just those in the selected form, class, or module.

The Assertion Sourcerer works in a very simple manner. When you use the Assertion Sourcerer menu option on the Add-Ins menu to request that assertion calls be added to your project, the Assertion Sourcerer automatically generates and adds a line after every Debug.Assert statement in your selected module (or the whole project). This line is a conversion of the Debug.Assert statement to a version suitable for calling the BugAssert procedure. So

Debug.Assert bTest = True

becomes

Debug.Assert bTest = True
BugAssert bTest = True, "bTest = True," _
          "Project Test.VBP, module Test.CLS, line 53"

BugAssert's first argument is just the assertion expression itself. The second argument is a string representation of that assertion. This is required because there is no way for Visual Basic to extract and report the assertion statement being tested from just the first argument. The final argument allows the BugAssert procedure to report the exact location of any assertion failure for later analysis. The BugAssert procedure that does this reporting is relatively simple. It uses a constant to not report assertion failures, to report them to a MsgBox, to report them to a disk file, or to report them to both.

Before compiling your executable, you'll need to set the constant mnDebug in the DEBUG.BAS module. Now whenever your executable is invoked by any other programmer, assertion failures will be reported to the location(s) defined by this constant. Before releasing your code into production, you can tell the Assertion Sourcerer to remove all BugAssert statements from your program.

Complete source code for the Assertion Sourcerer is supplied on the CD accompanying this book in CHAP06\assertion so that you can modify it to suit your own purposes.

Some final thoughts You can use the Assertion Sourcerer as a supplement to Debug.Assert when you want to implement assertions in compiled Visual Basic code.

Size Matters: The Metrics Sourcerer

Take any production system and log all the bugs it produces over a year or so. Then note which individual procedures are responsible for the majority of the defects. It's common for only 10 to 20 percent of a system's procedures to be responsible for 80 percent of the errors. If you examine the characteristics of these offending procedures, they will usually be more complex or longer (and sometimes both!) than their better-behaved counterparts. Keeping in mind that the earlier in the development cycle that these defects are detected the less costly it is to diagnose and fix them, any tool that helps to predict a system's problem areas before the system goes into production could prove to be very cost-effective. Step forward the Metrics Sourcerer. (See Figure 6-3.) This Visual Basic 6 add-in analyzes part or all of your project, ranking each procedure in terms of its relative complexity and length.

Figure 6-3 The Metrics Sourcerer dialog box

Defining complexity can be fairly controversial. Developers tend to have different ideas about what constitutes a complex procedure. Factors such as the difficulty of the algorithm or the obscurity of the Visual Basic keywords being employed can be considered useful to measure. The Metrics Sourcerer measures two rather more simple factors: the number of decision points and the number of lines of code that each procedure contains. Some evidence suggests that these are indeed useful characteristics to measure when you're looking for code routines that are likely to cause problems in the future. The number of decision points is easy to count. Certain Visual Basic 6 keywords—for example, If…Else…End If and Select Case—change the flow of a procedure, making decisions about which code to execute. The Metrics Sourcerer contains an amendable list of these keywords that it uses to count decision points. It then combines this number with the number of code lines that the procedure contains, employing a user-amendable weighting to balance the relative importance of these factors. The final analysis is then output to a text file, viewable by utilities such as WordPad and Microsoft Word. (By default, the filename is the name of your project with the extension MET.) You might also want to import the text file into Microsoft Excel for sorting purposes. There's no point in taking the output of the Metrics Sourcerer as gospel, but it would certainly be worthwhile to reexamine potentially dangerous procedures in the light of its findings.

Another factor that might be useful to measure is the number of assertion failures that each procedure in your program suffers from. This figure can be captured using the Assertion Sourcerer. Combining this figure with the numbers produced by the Metrics Sourcerer would be a very powerful pointer toward procedures that need more work before your system goes into production.

Some final thoughts Use the Metrics Sourcerer as a guide to the procedures in your programs that need to be examined with the aim of reducing their complexity. Economical to execute in terms of time, the Metrics Sourcerer can prove to be extremely effective in reducing the number of bugs that reach production.

A Black Box: The Instrumentation Sourcerer

When a commercial airliner experiences a serious incident or crashes, one of the most important tools available to the team that subsequently investigates the accident is the plane's black box (actually colored orange), otherwise known as the flight data recorder. This box provides vital information about the period leading up to the accident, including data about the plane's control surfaces, its instruments, and its position in the air. How easy would it be to provide this type of information in the event of user acceptance or production program bugs and crashes?

The Instrumentation Sourcerer, shown in Figure 6-4, walks through your program code, adding a line of code at the start of every procedure. This line invokes a procedure that writes a record of each procedure that is executed to a log file on disk. (See Chapter 1 for an in-depth examination of similar techniques.) In this way, you can see a complete listing of every button that your user presses, every text box or other control that your user fills in, and every response of your program. In effect, each program can be given its own black box. The interactive nature of Windows programs allows users to pick and choose their way through the different screens available to them. Thus it has traditionally been difficult to track exactly how the user is using your program or what sequence of events leads up to a bug or a crash. The Instrumentation Sourcerer can help you to understand more about your programs and the way they are used.

Figure 6-4 The Instrumentation Sourcerer dialog box

Configuration options allow you to selectively filter the procedures that you want to instrument. This might be useful if you want to document certain parts of a program, such as Click and KeyPress events. You can also choose how much information you want to store. Just as in an aircraft's black box, the amount of storage space for recording what can be a vast amount of information is limited. Limiting the data recorded to maybe the last 500 or 1000 procedures can help you to make the best use of the hard disk space available on your machine or on your users' machines.

Some final thoughts The Instrumentation Sourcerer can be useful in tracking the cause of program bugs and crashes, at the same time providing an effective record of how users interact with your program in the real world.


Final Thoughts

Is it possible to write zero-defect Visual Basic 6 code? I don't believe it is—and even if it were, I doubt it would be cost-effective to implement. However, it is certainly possible to drastically reduce the number of production bugs. You just have to want to do it and to prepare properly using the right tools and the right attitudes. From here, you need to build your own bug lists, your own techniques, and your own antidefect tools. Although none of the ideas and techniques described in this chapter will necessarily prevent you from creating a program with a distinct resemblance to a papcastle (something drawn or modeled by a small child that you are supposed to recognize), at least your programs will have presumptions toward being zero-defect papcastles.

Final disclaimer: All the bugs in this chapter were metaphorical constructs. No actual bugs were harmed during the writing of this work.


Required Reading

The following books are highly recommended—maybe even essential—reading for any professional Visual Basic developer.

  • Hardcore Visual Basic by Bruce McKinney (Microsoft Press, 1997)

  • Hitchhiker's Guide To Visual Basic and SQL Server, 6th Edition by Bill Vaughan (Microsoft Press, 1998)

  • Microsoft Visual Basic 6 Programmer's Guide by Microsoft Corporation (Microsoft Press, 1998)

  • Dan Appleman's Developing ActiveX Components With Visual Basic 5.0 by Dan Appleman (Ziff-Davis, 1997)

  • Software Project Survival Guide by Steve McConnell (Microsoft Press, 1997)

  • Code Complete by Steve McConnell (Microsoft Press, 1993)


Chapter 7 -- Minutiae

 Chapter 7

Minutiae

 Some Stuff About Visual Basic
PETER J. MORRIS

In addition to being a "doer," Peet also thinks and talks about writing code and is a frequent speaker at international conferences, such as VBITS and Microsoft's DevCon and TechEd. This book is Peet's second foray into the world of book writing—his first occurred about the time he was working at Microsoft when he wrote Windows: Advanced Programming and Design (now as rare as duck's teeth), which was a pure API, C, and Assembler SDK book.

As you can probably guess from its title, this chapter is going to cover a rather broad range of information about Visual Basic. Think of this chapter as a Visual Basic programmer's smorgasbord. You'll learn about such topics as the advantages and disadvantages of compiling to p-code and native code. You'll get some hints on how to optimize your applications beyond just writing excellent code. And you'll also receive up-to-the-minute information on such scintillating subjects as types and type libraries. So let's begin!


Stuff About the Compiler

In this section, we'll examine applications compiled to native code. We won't deal much with p-code (packed code) at all, aside from a brief introduction and some comparisons with native code.

As you probably know, Visual Basic 6 applications, just like their Visual Basic 5 counterparts, can now be "properly" compiled, unlike Visual Basic version 4 and lower, which produced p-code executables. In other words, as well as producing p-code executables, Visual Basic 6 can produce a native code binary. Which compile option you choose is up to you. I suspect that most corporate developers will want to know more about this compiler process than they ever wanted to know about p-code.

A Little About P-Code

P-code applications are usually smaller (and slower) than native code applications. With p-code, an interpreter compresses and packages your code. Then, at run time, this same interpreter expands and, of course, runs your application. P-code applications are usually ported more easily to different processors.

The term p-code was derived from the term "pseudocode" because p-code consists of a RISC-like set of instructions for a "make-believe" processor. At run time, this processor, usually known as a stack machine (because it uses a stack for practically all its operations), is simulated by the built-in interpreter. (Just so you know, a "normal" processor uses registers and a stack primarily to pass values to and from function calls.) Because of its imaginary nature, the processor's instruction set never needs to change; instead, each instruction is mapped, via a lookup table, to a real instruction on any given processor. Logically, then, all that's required to move code from one processor to another is this mapping—code generation remains largely unaffected.

In a nutshell, p-code is an intermediate step between the high-level instructions in your Visual Basic program and the low-level native code executed by your computer's processor. At run time, Visual Basic translates each p-code statement to native code.

With p-code, typical size reduction from native code is more than 50 percent. For example, when the VisData sample that is included on the Visual Basic 5 CD is compiled to p-code, the resulting executable is less than half the size it would be if compiled to native code (396 KB vs. 792 KB). Additionally, compiling p-code is a lot faster than compiling to native code—around seven times faster. (Some of the reasons for the speed of p-code compiling will become evident later in the chapter.) You'll need to keep this compile-time difference in mind during your development and testing phases. These compile timings, and all the other timings in this chapter, were made using prerelease builds of Visual Basic 6. If you're interested in these timings, you should conduct your own tests using the actual product.

Back in the days of Visual Basic 4 and earlier, a native code compiler was, I think, one of the most requested features, so I'm not surprised to find that Microsoft put it in Visual Basic 5—and, of course, the native code compiler is basically the same feature in Visual Basic 6. Personally, however, I think that native code compilation, for many reasons (and forgetting for a second that it typically executes faster) is a backward step. I'm still convinced that p-code is ultimately a superior technology compared to native code generation, as does, apparently, Sun Microsystems, because Java is essentially doing the same thing! Ever since the first version of Visual Basic, its p-code output has been, or could have been, roughly equivalent to Java's bytecodes. If a Visual Basic program had to be instantiated using the Visual Basic "virtual machine" (that is, something like vbrun100 <AppName>) and if that virtual machine were to be ported to different, non-Intel architectures, Visual Basic could have perhaps led the way to what's now become "bytecode nerdvana" instead of being criticized in its p-code form for being both slow and interpreted (just like pure Java is, in fact) Not convinced? Here's one of Sun's own descriptions of bytecode technology.

[On Java being a portable technology] "The Java compiler does this by generating bytecode instructions that have nothing to do with a particular computer architecture. Rather, they are designed to be both easy to interpret on any machine and easily translated into native machine code on the fly." I'm sure the similarity between the two is obvious.

If you want to read some more stuff about p-code that isn't specific to Visual Basic, search MSDN for "Microsoft P-Code Technology" and see Andy Padawer's excellent paper on the subject.

Generating Code

You select the code generation model you want via the somewhat hidden dialog box shown in Figure 7-1. You get to this dialog box by choosing Properties from the Project menu.

Figure 7-1 Visual Basic's compiler options dialog boxes

As you can see, some extra compilation options become available when you select Compile To Native Code. I'll discuss some of these options a little later.

When you compile to native code, the Visual Basic 6 native code generator/compiler, C2.EXE, is run once for each code component in the project. For example, if a project has a form, Form1; a standard module, Module1; and a class module, Class1; C2.EXE is run a total of three times. Each invocation's options are the same depending on which you selected in the dialog box; that is, the options you select are used to compile the entire project. In case you're interested, C2.EXE runs as a multithreaded, Win32, 32-bit console process.

Each time the native code compiler is run, a hidden process (described as 16-bit by the Windows 95 Task Manager) is started and the code generator/compiler, also run as a hidden process, is run attached to this process. (In Windows 95, this process is run from the file WINOA386.MOD, with a process description of "Non-Windows application component for 386 enhanced mode." This file is not required if you're running under Windows NT.) As each invocation of C2.EXE terminates, the instance of WINOLDAP (the module name given to WINOA386.MOD) in which it was run is also terminated. You should now start to see why this process might be slower than selecting p-code generation (which is an internal process and doesn't use C2.EXE, although it does use LINK.EXE). Here's what the command-line arguments of a typical compilation look like (with no optimizations):

C2 -il C:\WINDOWS\TEMP\VB603389 -f Form1 -W3 -Gy -G5 -Gs4096
-dos -Zl -FoC:\TEMP\Form1.OBJ -QIfdiv -ML -basic

These flags are explained in Table 7-1.

Table 7-1. Command-line flags for the C2 Compiler
Flag | Explanation
-il C:\WINDOWS\TEMP\VB603389 | Undocumented but also used for C program; probably used to "name" intermediate language files
-f Form1 | The input file to be compiled
-W3 | Warning level 3
-Gy | Enable function-level linking
-G5 | Optimize for Pentium
-Gs4096 | Turn off stack probes
-dos | Undocumented but also used for a C program
-Zl | Remove default library name from OBJ file
-Fo C:\TEMP\Form1.OBJ | Name of output file
-QIfdiv | Perform Pentium FDIV erratum fix
-ML | Create a single-threaded executable file
-basic | Undocumented but appears to be a new flag for Visual Basic compilation

Some of the flags are described in more detail here as well:

-il This flag is undocumented but "intermediate language" is a good guess for what "il" stands for. Files produced are <Signature>GL, SY, EX, IN, and DB. I have no idea what these files contain. In the command-line example in Table 7-1, the following files (long filenames shown) are generated temporarily while the application is being built:

  • VB603389GL

  • VB603389SY

  • VB603389EX

  • VB603389IN

  • VB603389DB

-G5 The option optimizes the generated code to favor the Intel Pentium processor. Here's what the Microsoft Developer Network (MSDN) says about the same Visual C++ flag: "Use this option for programs meant only for the Pentium. Code created using the /G5 option does not perform as well on 80386- and 80486-based computers as code created using the /GB (Blend) option." Interestingly, by default, the -G5 switch is always used—even when you compile on a 486 machine.

-Gs[size] If a function requires more than size stack space for local variables, its stack probe is activated. A stack probe is a piece of code that checks whether the space required for passed parameters and local variables is available on the stack before any attempt to allocate the space is made. -Gs0 is the same as -Ge, turn stack probes on; -Gs4096 is the default.

-ML This option places the library name LIBC.LIB in the object file so that the linker will use LIBC.LIB to resolve external symbols. This is the compiler's default action. LIBC.LIB does not provide multithread support, by the way.

Don't bother to scan your Visual Basic 6 documentation for information about these flags because you won't find any—they are all undocumented. If you have a set of documentation for the Visual C++ compiler, however, you might be in luck. It seems that C2.EXE is taken from the Visual C++ compiler (this file is called C2.DLL in version 6 of Visual C++, although in Visual Basic 5, both Visual Basic and Visual C++ shared exactly the same file—C2.EXE). C2.EXE is, in fact, the compiler from Microsoft's Visual C++ product. Nevertheless, the above interpretation of the flag meanings is mine alone. Microsoft doesn't document how its C++ compiler works beyond describing CL.EXE (the front end to the C compiler).

Table 7-2 provides a summary of the C2 compiler incarnations at the time of this book's writing.

Table 7-2. Comparison of Visual C++ and Visual Basic C2 Components
Component | Product Version | Compiler Description
C2.EXE (from Visual Basic 6) | 6.0.8041.0 | 32-Bit 80x86 Compiler Back End
C2.DLL (from Visual C++ 6) | 6.0.8168.0 | 32-Bit 80x86 Compiler Back End
C2.EXE (from Visual Basic 5) | 5.0.0.7182 | 32-bit Visual Basic Compiler Back End

Visual Basic itself evidently provides the compiler's first pass, unlike Visual C++ in which the first pass (the parser and some of the optimizer) of C and C++ files is provided by either C1.DLL or C1XX.DLL, respectively. In terms of compilers, VB6.EXE is seemingly analogous to CL.EXE.

The Loggers

Either the C application or the Visual Basic application listed at the end of this section (and on the CD) can be used to replace the real C2.EXE file. To replace it, follow these steps for the C version:

  1. Make backup copies of C2.EXE and LINK.EXE.

  2. Rename C2.EXE to C3.EXE.

  3. If you want to rebuild the C application, make sure that the first real line of code in the OUTARGS.C source file reads as follows:

    strcpy(&carArgs[0], ".\\C3 ");

    The binary version on the CD already includes this line of code.

  4. Copy the EXE (OUTARGS.EXE) to C2.EXE.

    copy outargs.exe c2.exe

  5. Your original C2.EXE is now C3.EXE, so no damage is done.

    Use Visual Basic 6 as you normally would.

The steps for using the Visual Basic version are a little different. To replace C2.EXE with the Visual Basic application, follow these steps:

  1. Make backup copies of C2.EXE and LINK.EXE.

  2. Compile the code to OUTARGS.EXE (make sure your project contains just the OUTARGS.BAS standard module—no forms or anything else).

  3. Rename C2.EXE to C3.EXE. Rename LINK.EXE to L1NK.EXE. (Note that the "i" has been changed to a "1".)

  4. Copy the EXE (OUTARGS.EXE) to C2.EXE and LINK.EXE. Your original C2.EXE is now C3.EXE and your LINK.EXE is now L1NK.EXE, so no damage is done.

  5. Run REGEDIT.EXE, and under HKEY_CURRENT_USER\Software\VB and VBA Program Settings insert two new keys (subkeys and values as shown here):

    HKEY_CURRENT_USER\Software\VB and VBA Program Settings\
          \C2
             \Startup
                \RealAppName    ".\C3"
          \LINK
              \Startup
                \RealAppName    ".\L1NK"

  6. Use Visual Basic 6 as normal.

The purpose of the Visual Basic version of OUTARGS.EXE is to have the same binary self-configure from a Registry setting. This means that you only need one OUTARGS.EXE (renamed appropriately) to "spy" on any application.

The output of the Visual Basic application is a little less fully featured than that produced by the C application. After you've carried out either of these steps, the following will happen: When Visual Basic 6 runs (to compile to native code), it will run C2.EXE. C2.EXE, which is really our OUTARGS.EXE program, will log the call made to it to the file C2.OUT. (Our application logs to a file based upon its own name, <EXEname>.OUT; because our application is renamed C2.EXE, the log file will be C2.OUT.) Information logged includes the parameters that have been passed to it. C2.EXE will then shell C3.EXE (the "real" C2), passing to it, by default, all the same parameters that it was passed. The net effect is that you have logged how C2 was invoked.

The Visual Basic OUTARGS program will also be used to log the linker, if you followed the steps above. Listing 7-1 is a typical C2.OUT log (C version).

Listing 7-1 Typical C2.OUT log file
********** Run @ Wed Jan 1 00:00:00 1998

* EXE file...

        C2

* Command Line Arguments...

1       -il
2       C:\WINDOWS\TEMP\VB476314
3       -f
4       Form1
5       -W
6       3
7       -Gy
8       -G5
9       -Gs4096
10      -dos
11      -Zl
12      -FoC:\TEMP\Form14.OBJ
13      -QIfdiv
14      -ML
15      -basic
* 'Real' program and arguments...

        .\C3 -il C:\WINDOWS\TEMP\VB476314 -f Form1 -W 3 -Gy -G5
        -Gs4096 -dos -Zl -FoC:\TEMP\Form14.OBJ -QIfdiv -ML -basic

********** Run End

The Visual Basic team seems to have added a space between the -W and the 3, possibly causing C2 to interpret this as two separate switches. Since C2 doesn't error or complain, I'm assuming that it knows to treat the switch as W3 (warning level set to 3).

By further altering the code (again, the C version is demonstrated here), you can change, add, or remove compiler switches. For example, you can add the following code to the argument processing loop to replace, say, -G5 with, say, -GB, the "blend" switch mentioned earlier in our discussion of -G5.

if (0 == strcmp(argv[nLoop], "-G5"))
{
    (void)strcat(&carArgs[0], "-GB ");

    continue;
}

NOTE

The C version OUTARGS.EXE doesn't like long pathnames that include spaces. Each "gap" causes the next part of the pathname to be passed to C3 as a separate command-line argument. To fix this, either alter the C code to quote delimit each pathname or copy your test Visual Basic project to, say, C:\TEMP before attempting to use it; that is, remove any long pathname. (Leave the renamed OUTARGS C2.EXE in the same folder as the real, now renamed, C3.EXE.) Note that the Visual Basic OUTARGS.EXE doesn't have the same problem.

To restore the "real" program, simply copy over C2.EXE with C3.EXE:

copy c3.exe c2.exe

The Linker

As I've already said, C2.EXE compiles each component to an object file. When all the components are compiled, they are linked using LINK.EXE. Table 7-3 lists the command line arguments you might find in a typical run when creating an EXE containing a single form, class module, and standard module. The only compile option switched on for this run was Create Symbolic Debug Info. This information was captured using the OUTARGS.EXE program.

Again, LINK.EXE is taken from the Visual C++ 6.0 compiler. At the time of writing, its version number was 6.00.8168.0—exactly the same version as that supplied with C2.DLL. See the Visual C++ documentation or MSDN for more information regarding these linker switches.

The linker is also used to create a p-code application, by the way. The difference in the invocation is that VBAEXE6.LIB is not linked in and that only one object file is used as input—ProjectName.OBJ.

Table 7-3 Command-Line Switches for the Linker
Switch | Explanation
C:\TEMP\Form1.OBJ | Form OBJ file
C:\TEMP\Module1.OBJ | Module OBJ file
C:\TEMP\Class1.OBJ | Class OBJ file
C:\TEMP\Project1.OBJ | Project OBJ file
C:\PROGRAM FILES\VISUAL STUDIO\VB\VBAEXE6.LIB | Library of Visual Basic OBJs
/ENTRY:__vbaS | Sets the starting address for an executable file or DLL. The entry point should be a function that is defined with the stdcall calling convention. The parameters and the return value must be defined as documented in the Win32 API for WinMain (for an . EXE) or DllEntryPoint (for a DLL). This entry point is in your <project name>.OBJ file—here it will be in PROJECT1.OBJ. Note that neither Sub Main nor Form_Load is mentioned.
/OUT:C:\TEMP\Project1.exe | The output file—the EXE!
/BASE:0x400000 | Sets a base address for the program, overriding the default location for an executable file (at 0x400000) or a DLL (at 0x10000000). The operating system first attempts to load a program at its specified or default base address. If sufficient space is not available there, the system relocates the program. To prevent relocation, use the /FIXED option. The BASE generated by Visual Basic 6 for an ActiveX DLL is 0x11000000—something that's different from the default at last.
/SUBSYSTEM:WINDOWS,4.0 | Tells the operating system how to run the .EXE file. (Options include CONSOLE | WINDOWS | NATIVE | POSIX.)
/VERSION:1.0 | Tells the linker to put a version number in the header of the executable file or DLL. (This option has nothing to do with a VERSIONINFO resource.) The major and minor arguments are decimal numbers in the range 0 through 65535. The default is version 0.0. Visual Basic uses the Major and Minor settings on the Make tab of the Project Properties dialog box for these values. This switch is used to document the image version as shown by DUMPBIN.EXE (another Microsoft Visual C++ tool).
/DEBUG | Creates debugging information for the executable file or DLL. The linker puts the debugging information into a program database (PDB). It updates the program database during subsequent builds of the program.
/DEBUGTYPE:{CV|COFF|BOTH} | Generates debugging information in one of three ways: Microsoft format, COFF format, or both. CV is CodeView; COFF is Common Object File Format.
/INCREMENTAL:NO | Specifies whether incremental linking is required.
/OPT:REF | Excludes unreferenced packaged functions from the executable file. Packaged functions are created using the Gy flag at compile time (see Table 7-1). Packaged functions have several uses (not mentioned here) and are created automatically, sometimes by the compiler. For example, C++ member functions are automatically packaged.
/MERGE:from=to | Combines the first section (from) with the second section (to), naming the resulting section "to". If the second section does not exist, LINK renames the section "from" as "to". The /MERGE option is most useful for creating VxDs and for overriding the compiler-generated section names.
/IGNORE:4078 | Ignores certain warnings (defined in LINK.ERR). 4078 means that LINK found two or more sections that have the same name but different attributes.

Why these switches?

I have no idea why some of these switches are used explicitly (on the compiler also), particularly since some are set to the default anyway. Perhaps some of the reasons for using these switches will be documented at later.

Using the Compiler to Optimize Your Code

The effect of the optimization options (on the Compile tab of the Project Properties dialog box and in the Advanced Optimizations dialog box) on how C2.EXE and LINK.EXE are driven is summarized in Table 7-4 (for building a standard EXE).

Obviously, -G6 means favor the Pentium Pro.

Notice that most of the switches have no effect on how C2 or LINK are started (although the EXE size changes so that we know the option is making itself known!). Since most switches have no effect, we must assume they are being acted on within VB6.EXE itself (as it seems to contain the compiler's first pass). Or perhaps the mystery files shown earlier (VB603389GL, VB603389SY, VB603389EX, VB603389IN, and VB603389DB) have some way of influencing the code generator, thus sidestepping our efforts to understand how the process is being controlled.

Table 7-4 The Compiler Effect
Optimization Option | C2.EXE Effect | LINK.EXE Effect
Optimize For Small Code | None | None
Optimize For Fast Code | None | None
Favor Pentium Pro | /G6 (from G5) | None
Create Symbolic Debug Info | /Zi | /DEBUG
/DEBUGTYPE:CV |  |
Assume No Aliasing | None | None
Remove Array Bounds Checks | None | None
Remove Integer Overflow Checks | None | None
Remove Floating Point Error Checks | None | None
Allow Unrounded Floating Point Operations | None | None
Remove Safe Pentium(tm)FDIV Checks | /QIfdiv Removed | None

Advanced Optimizations

Microsoft generally encourages you to play around with what they call the safe compiler options. Naturally, these are options that aren't situated beneath the Advanced Optimizations button. For those options Microsoft usually provides a disclaimer: "These might crash your program." Let's see what these Advanced Optimizations are about and why this warning is given. (See Table 7-5)

Table 7-5 Advanced Optimizations Options
Option | Description
Allow Unrounded Floating Point Operations | Allows the compiler to compare floating-point expressions without first rounding to the correct precision. Floating-point calculations are normally rounded off to the correct degree of precision (Single or Double) before comparisons are made. Selecting this option allows the compiler to do floating-point comparisons before rounding, when it can do so more efficiently. This improves the speed of some floating-point operations; however, this may result in calculations being maintained to a higher precision than expected, and two floating-point values not comparing equal when they might be expected to.
Assume No Aliasing | Tells the compiler that your program does not use aliasing (that your program does not refer to the same memory location by more than one name, which occurs when using ByRef arguments that refer to the same variable in two ways). Checking this option allows the compiler to apply optimization such as storing variables in registers and performing loop optimizations.
Remove Array Bounds Checks | Disables Visual Basic array bounds checking. By default, Visual Basic makes a check on every access to an array to determine if the index is within the range of the array. If the index is outside the bounds of the array, an error is returned. Selecting this option will turn off this error checking, which can speed up array manipulation significantly. However, if your program accesses an array with an index that is out of bounds, invalid memory locations might be accessed without warning. This can cause unexpected behavior or program crashes.
Remove Floating Point Error Checks | Disables Visual Basic floating-point error checking and turns off error checking for valid floating-point operations and numeric values assigned to floating-point variables. By default in Visual Basic, a check is made on every calculation to a variable with floating-point data types (Single and Double) to be sure that the resulting value is within the range of that data type. If the value is of the wrong magnitude, an error will occur. Error checking is also performed to determine if division by zero or other invalid operations are attempted. Selecting this option turns off this error checking, which can speed up floating-point calculations. If data type capacities are overflowed, however, no error will be returned and incorrect results might occur.
Remove Integer Overflow Checks | Disables Visual Basic integer overflow checking. By default in Visual Basic, a check is made on every calculation to a variable with an integer data type (Byte, Integer, Long, and Currency) to be sure that the resulting value is within range of that data type. If the value is of the wrong magnitude, an error will occur. Selecting this option will turn off this error checking, which can speed up integer calculations. If data type capacities are overflowed, however, no error will be returned and incorrect results might occur.
Remove Safe Pentium FDIV Checks | Disables checking for safe Pentium floating-point division and turns off the generation of special code for Pentium processors with the FDIV bug. The native code compiler automatically adds extra code for floating-point operations to make these operations safe when run on Pentium processors that have the FDIV bug. Selecting this option produces code that is smaller and faster, but which might in rare cases produce slightly incorrect results on Pentium processors with the FDIV bug.
By using the Visual C++ debugger (or any compatible debugger) with Visual Basic code that has been compiled to contain symbolic debugging information, it's possible to see more of what each option does to your code. By way of explanation, here are a few annotated examples (obviously you won't expect to see commented code like this from a debugger!):

Integer Overflow

Dim n As Integer

n = 100 * 200 * 300

Disassembly (without Integer Overflow check)

' Do the multiplication - ax = 300
mov         ax,offset    Form::Proc+4Ch

' Signed integer multiplication. 300 * 20000
' The 20000 is stored in Form::Proc+51h and was
' created by the compiler from the constant exp.
' 100 * 200 and held as 'immediate data'
imul        ax,ax,offset Form::Proc+51h

n = Result

mov         word ptr [n],ax

Disassembly (with Integer Overflow check)

' Do the multiplication - ax = 100
mov         ax,offset    Form::Proc+4Ch
imul        ax,ax,offset Form::Proc+51h

' Jump to error handler if the overflow flag set
jo          ___vbaErrorOverflow

Else, n = Result
mov         word ptr [n],ax

Array Bounds

  Dim n1        As Integer
  Dim n(100)    As Integer

  n1 = n(101)

Disassembly (without Array Bounds check)

' Sizeof(Integer) = 2, put in eax
push        2
pop         eax

' Integer multiplication.  2 * 101 (&H65) = result in eax.
imul        eax,eax,65h

' Get array base address in to ecx.
mov         ecx,dword ptr [ebp-20h]

n(101) (base plus offset) is in ax
mov         ax,word ptr [ecx+eax]

n1 = n(101)
mov         word ptr [n1],ax

Disassembly (with Array Bounds check)

' Address pointed to by v1 = 101, the offset we want
mov         dword ptr [unnamed_var1],65h

' Compare value thus assigned with the known size of array + 1
cmp         dword ptr [unnamed_var1],65h

' Jump above or equal to 'Call ___vbaGenerateBoundsError'
jae         Form1::Proc+6Dh

' Zero the flags and a memory location.
and         dword ptr [ebp-48h],0

' Jump to 'mov eax,dword ptr [unnamed_var1]'
jmp         Form1::Proc+75h

' Raise the VB error here
call        ___vbaGenerateBoundsError

' Store element number we want to access
mov         dword ptr [ebp-48h],eax

' Get the element we wanted to access into eax
mov         eax,dword ptr [unnamed_var1]

' Get array base address in to ecx.
mov         ecx,dword ptr [ebp-20h]

' n(101) is in ax (* 2 because sizeof(Integer) = 2
mov         ax,word ptr [ecx+eax*2]

' n1 = n(101)
mov         word ptr [n1],ax
Floating Point Error
Dim s As Single

s = s * s

Disassembly (without Floating Point Error check)

' Pushes the specified operand onto the FP stack
fld         dword ptr [s]

' Multiplies the source by the destination and returns
' the product in the destination
fmul        dword ptr [s]

' Stores the value in the floating point store (ST?)
' to the specified memory location
fstp        dword ptr [s]

Disassembly (with Floating Point Error check)

fld         dword ptr [s]
fmul        dword ptr [s]
fstp        dword ptr [s]

' Store the floating point flags in AX (no wait)
fnstsw      ax

' Test for floating point error flag set
test        al,0Dh

' Jump if zero flag not set
jne         ___vbaFPException

You should now have more of a feel for why these options are left partially obscured like they are, and for the warning given by Microsoft. Without a native code debugger it's really hard to see just how your code's being affected. Even with a debugger like the one that comes with Visual Studio it's not a straightforward task to read through the assembly-language dumps and state that your code is cool and that the optimizations you've chosen are safe!

Library and object Files

From Table 7-3, you'll notice that VBAEXE6.LIB is linked in with our own OBJ file (created from our files and modules). The library contains just one component (library files contain object files), NATSUPP.OBJ. (NATSUPP might stand for "native support.") You can find this object by using DUMPBIN /ARCHIVEMEMBERS VBAEXE6.LIB. (DUMPBIN.EXE is the Microsoft Common Object File Format [COFF] Binary File Dumper.) NATSUPP.OBJ can be extracted for further examination using the Microsoft Library Manager, LIB.EXE:

lib /
extract:c:\vbadev\r6w32nd\presplit\vbarun\obj\natsupp.obj vbaexe6.lib

The reason for including the path to the OBJ file is that the library manager expects us to specify exactly the name of the module—including its path. (This is embedded into the library file when the object file is first put into it and is discovered using DUMPBIN /ARCHIVEMEMBERS.) In other words, the object file probably "lived" at this location on someone's machine in Redmond! Similarly, we can tell that the source code for this object file was named NATSUPP.ASM and was in the directory C:\VBADEV\RT\WIN32. It was assembled using Microsoft's Macro Assembler, Version 6.13. (6.11 is the latest version available to the public, I believe.) Interestingly, it doesn't contain any code—just data—although what looks like a jump table (a mechanism often used to facilitate calls to external routines) appears to be included. To call a routine, you look up its address in the table and then jump to it, as shown in Table 7-6.

Table 7-6 Contents of NATSUPP.OBJ
Name | Size | Content
.text | 0 | Readable code
.data | 4 | Initialized readable writable data
.debug$S | 140 | Initialized discardable readable data
.debug$T | 4 | Initialized discardable readable data
The sections are as follows:

  • .text is where all the general-purpose code created by the compiler is output. (It's 0 bytes big, which probably means no code!)

  • .data is where initialized data is stored.

  • .debug$S and .debug$T contain, respectively, CodeView Version 4 (CV4) symbolic information (a stream of CV4 symbol records) and CV4 type information (a stream of CV4 type records), as described in the CV4 specification.

As well as statically linking with this library file, other object files reference exported functions in yet another library file, MSVBVM60.DLL This is a rather large DLL installed by the Visual Basic 6 Setup program in the WINDOWS\SYSTEM directory. (The file describes itself as Visual Basic Virtual Machine and at the time of writing was at version 6.0.81.76—or 6.00.8176 if you look a the version string.) Using DUMPBIN /EXPORTS MSVBVM60.DLL on this DLL yields some interesting symbolic information. For example, we can see that it exports a number of routines, 635 in fact! Some interesting-looking things, possibly routines for invoking methods and procedures, are in here as well: MethCallEngine and ProcCallEngine. Additionally, there are what look like stubs, prefixed with rtc ("run-time call," perhaps?), one for apparently all the VBA routines: rtcIsArray, rtcIsDate, rtcIsEmpty, … rtcMIRR , … rtcMsgBox, … rtcQBColor, and so on. And as with most DLLs, some cryptic, yet interesting exports, such as Zombie_Release, are included.

In addition to this symbolic information, the DLL contains a whole bunch of resources, which we can extract and examine using tools such as Visual C++ 6. Of all the resources the DLL contains, the one that really begs examination is the type library resource. If we disassemble this using OLEVIEW.EXE, we can see its entire type library in source form.

The type library contains all sorts of stuff as well as the interface definitions of methods and properties, such as the hidden VarPtr, ObjPtr, and StrPtr routines.

It turns out that this MSVBVM60.DLL is probably the run-time support DLL for any Visual Basic 6 native and p-code executable; that is, it acts like MFC42.DLL does for an MFC application. (MFC stands for Microsoft Foundation Classes, Microsoft's C++/Windows class libraries.) We can confirm this by dumping a built native code executable. Sure enough, we find that the executable imports routines from the DLL. (By the way, the Package And Deployment Wizard also lists this component as the Visual Basic Runtime.)

By dumping other separate object files, we can gather information about what is defined and where it is exported. For example, we can use DUMPBIN /SYMBOLS MODULE1.OBJ to discover that a function named Beep will be compiled using Microsoft's C++ name decoration (name mangling) regime and thus end up being named ?Beep@Module1@@AAGXXZ. Presumably, this function is compiled as a kind of C++ anyway; that is, in C++ it is defined as (private: void __stdcall Module1::Beep(void)). Or better yet, we can use DUMPBIN /DISASM ????????.OBJ to disassemble a module.

The same routine—Beep—defined in a class, Class1 for example, looks like this:

?Beep@Class1@@AAGXXZ (private: void __stdcall Class1::Beep(void)).

Maybe now we can see why, since Visual Basic 4, we've had to name modules even though they're not multiply instantiable. Each seems to become a kind of C++ class. According to the name decorations used, Beep is a member of the C++ Classes Class1 and Module1.

The Logger Code

As promised, Listing 7-2 shows the C source code for the spy type application we used earlier on the command line arguments of both C2.EXE and LINK.EXE. Note that a nearly equivalent Visual Basic version follows this application.

Listing 7-2 The OUTARGS logger application in C

/*******************************************************

Small 'C' applet used to replace Visual Basic 6 compiler
apps so as to gather their output and manipulate their
command-line switches.

See notes in main text for more details.

*******************************************************/

#include < stdio.h   >
#include < string.h  >
#include < time.h    >
#include < windows.h >

int main
(
    int    argc     // Number of command-line arguments.
   ,char * argv[]   // The arguments themselves.
   ,char * env []   // Environment variables.
)

{
    /**************************************
    ** General declares.
    */
    #define BUFF 2048

    auto FILE *      stream;        // File to write to.

    auto struct tm * tt;            // Time stuff for time of write.
    auto time_t      t;             // ----- " " -----

    auto char        carBuff[255];  // Used for holding output
                                    // file name.

    auto char        carArgs[BUFF]; // Holds command line args
                                    // for display.
    auto int         nLoop;         // Loop counter.

    /* ***************
    ** Code starts ...
    */

    // Change according to what real (renamed) application you
    // want to start.
    (void)strcpy(&carArgs[0], ".\\C3 ");
    // Get the system time and convert it to ASCII string.
    (void)time(&t);
    tt = localtime(&t);

    // Going to need to append to our exe name, so write
    // to temp buffer.
    (void)strcpy(&carBuff[0], argv[0]);

    // Now append .OUT - should contain ???.OUT after this where ???
    // could be APP.EXE or just APP, depending upon how this program
    // is run.
    (void)strcat(&carBuff[0], ".OUT");

    // Write to EXEName.OUT file (append mode)...
    if (NULL != (stream = fopen(&carBuff[0], "a")))
    {
        // Write out the time.
        (void)fprintf(stream, "********** Run @ %s\n", asctime(tt));

        // Output name of EXE file.
        (void)fprintf(stream, "* EXE file...\n\n");
        (void)fprintf(stream, "\t%s\n", argv[0]);

        /* *****************************************************
        ** Output command line args (exclude our exe name argv[0]).
        */

        (void)fprintf(stream, "\n* Command Line Arguments...\n\n");

        for (nLoop = 1; nLoop < argc; nLoop++)
        {
            (void)fprintf(stream,"%d\t%s\n", nLoop, argv[nLoop]);

            // Append to args buffer.
            (void)strcat(&carArgs[0]   , argv[nLoop]);
            (void)strcat(&carArgs[0]   , " ");
        }

        /* *****************************
        ** Output environment variables.
        */

        (void)fprintf(stream, "\n* Environment Variables...\n\n");

        for (nLoop = 0; NULL != env[nLoop]; nLoop++)
        {
          (void)fprintf(stream, "%d\t%s\n", nLoop, env[nLoop]);
        }

        /* ***************************************************
        ** Output name and args of other application to start.
        */

        (void)fprintf(stream, "\n* 'Real' program and arguments...\n\n");
        (void)fprintf(stream, "\t%s\n", &carArgs[0]);

        (void)fprintf(stream, "\n********** Run End\n\n\n");

        // All done so tidy up.
        (void)fclose(stream);

        (void)WinExec(&carArgs[0], 1);
    }

    return 0;
}

And the (nearly) equivalent Visual Basic application in Listing 7-3:

Listing 7-3 The OUTARGS logger application in Visual Basic

Sub Main()
    If 0 <> Len(Command$) Then

        Dim sRealAppName As String

        sRealAppName = GetSetting(App.EXEName, "Startup", _
                                  "RealAppName", "")

        If 0 <> Len(sRealAppName) Then

            Call Shell(sRealAppName & " " & Command$, vbHide)

            Dim nFile As Integer

            nFile = FreeFile

            Open App.EXEName & ".out" For Append Access Write As nFile

            Print #nFile, "****** Run at " & _
                          Format$(Date, "Short date") & _
                          " " & Format$(Time, "Long Time")

            Print #nFile, sRealAppName & " " & Command$

            Close nFile

        End If

    End If

End Sub


Stuff About Optimization

This section deals with how to best optimize your applications. Notice that the word "code" didn't appear in the preceding sentence. To correctly optimize the way we work and the speed with which we can ship products and solutions, we need to look beyond the code itself. In the following pages, I'll describe what I think are the most effective ways to optimize applications.

Choosing the Right Programmers

In my opinion, there's a difference between coding and programming. Professional programming is all about attitude, skill, knowledge, experience, and last but most important, the application of the correct algorithm. Selecting the right people to write your code will always improve the quality, reuse, and of course execution time of your application. See Chapter 17 (on recruiting great developers) for more on this subject.

Using Mixed Language Programming

Correctly written Visual Basic code can easily outperform poorly written C code. This is especially true with Visual Basic 6. (Visual Basic 6 native code is faster than p-code.) Whatever language you use, apply the correct algorithm.

At times, of course, you might have to use other languages, say, to gain some required speed advantage. One of the truly great things about Windows (all versions) is that it specifies a linkage mechanism that is defined at the operating system level. In MS-DOS, all linkages were both early and defined by the language vendor. The result was that mixed-language programming was something that only the very brave (or the very foolish) would ever have attempted. It used to be impossible, for example, to get some company's FORTRAN compiler to produce object files that could be linked with other object files generated by another company's C compiler. Neither the linker supplied with the FORTRAN compiler nor the one that came with the C compiler liked the other's object file format. The result was that mixed-language programming was almost impossible to implement. This meant, of course, that tried-and-tested code often had to be ported to another language (so that the entire program was written in one language and therefore linked).

Trouble is that these days we've largely forgotten that mixed language programming is even possible. It is! Any language compiler that can produce DLLs can almost certainly be used to do mixed-language programming. For example, it's now easy to call Microsoft COBOL routines from Visual Basic. Similarly, any language that can be used to create ActiveX components can be used to create code that can be consumed by other, language-independent, processes.

At The Mandelbrot Set (International) Limited (TMS), when we really need speed—and after we've exhausted all the algorithmic alternatives—we turn to the C compiler. We use the existing Visual Basic code as a template for writing the equivalent C code. (We have an internal rule that says we must write everything in Visual Basic first—it's easier, after all.) We then compile and test (profile) this code to see whether the application is now fast enough. If it's not, we optimize the C code. Ultimately, if it's required, we get the C compiler to generate assembly code, complete with comments (/Fc and /FA CL.EXE switches are used to do this), and discard the C code completely. Finally, we hand-tune the assembly code and build it using Microsoft's Macro Assembler 6.11.

Controlling Your Code's Speed

Don't write unnecessarily fast code. What I mean here is that you shouldn't produce fast code when you don't need to—you'll probably be wasting time. Code to the requirement. If it must be fast, take that into account as you code—not after. If it's OK to be slow(er), then again, code to the requirement. For example, you might decide to use nothing but Variants if neither size nor execution speed is important. Such a decision would simplify the code somewhat, possibly improving your delivery schedule. Keep in mind that each project has different requirements: code to them!

Putting On Your Thinking Cap

The best optimizations usually happen when people really think about the problem1. I remember once at TMS we had to obtain the sine of some number of degrees many times in a loop. We used Visual Basic's Sin routine to provide this functionality and ultimately built the application and profiled the code. We found that about 90 percent all our recalculating execution time was spent inside the Sin routine. We decided therefore to replace the call to Visual Basic's routine with a call to a DLL function that wrapped the C library routine of the same name. We implemented the DLL, rebuilt, and retested. The results were almost identical. We still spent most of the time inside the Sin routine (although now we had another external dependency to worry about—the DLL!). Next we got out the C library source code for Sin and had a look at how we might optimize it. The routine, coded in an assembly language, required detailed study—this was going to take time! At this point, someone said, "Why don't we just look up the required value in a previously built table?" Brilliant? Yes! Obvious? Of course! 1. See the famous book Programming Pearls by Jon Bentley for more on this approach. (Addison-Wesley, 1995, ISBN 0-201-10331-1.)

Staying Focused

Don't take your eyes off the ball. In the preceding example, we lost our focus. We got stuck in tune mode. We generated the lookup table and built it into the application, and then we rebuilt and retested. The problem had vanished.

"Borrowing" Code

Steal code whenever possible. Why write the code yourself if you can source it from elsewhere? Have you checked out MSDN and all the sample code it provides for an answer? The samples in particular contain some great (and some not so great) pieces of code. Unfortunately, some programmers have never discovered the VisData sample that shipped with Visual Basic 5, let alone looked through the source code. If you have Visual Basic 5, let's see if I can tempt you to browse this valuable resource. VISDATA.BAS contains the following routines. Could they be useful?
ActionQueryType | AddBrackets | AddMRU
CheckTransPending | ClearDataFields | CloseAllRecordsets
CloseCurrentDB | CompactDB | CopyData
CopyStruct | DisplayCurrentRecord | DupeTableName
Export | GetFieldType | GetFieldWidth
GetINIString | GetODBCConnectParts | GetTableList
HideDBTools | Import | ListItemNames
LoadINISettings | NewLocalISAM | MakeTableName
MsgBar | OpenLocalDB | NewMDB
ObjectExists | RefreshErrors | OpenQuery
OpenTable | SetFldProperties | RefreshTables
SaveINISettings | ShowError | SetQDFParams
ShowDBTools | StripConnect | ShutDownVisData
StripBrackets | StripOwner | StripFileName
StripNonAscii | stTrueFalse | UnloadAllForms
vFieldVal |  |

There's more! The BAS files in the SETUP1 project contain these routines—anything useful in here?
AbortAction | AddActionNote | AddDirSep
AddHkeyToCache | AddPerAppPath | AddQuotesToFN
AddURLDirSep | CalcDiskSpace | CalcFinalSize
CenterForm | ChangeActionKey | CheckDiskSpace
CheckDrive | CheckOverwrite-PrivateFile | CommitAction
CopyFile | CopySection | CountGroups
CountIcons | CreateIcons | CreateOSLink
CreateProgManGroup | CreateProgManItem | CreateShellLink
DecideIncrement-RefCount | DetectFile | DirExists
DisableLogging | EnableLogging | EtchedLine
ExeSelfRegister | ExitSetup | Extension
fCheckFNLength | fCreateOS-ProgramGroup | fCreateShellGroup
FileExists | fIsDepFile | fValidFilename
FValidNT-GroupName | fWithinAction | GetAppRemo-valCmdLine
GetDefMsgBoxButton | GetDepFileVerStruct | GetDiskSpaceFree
GetDrivesAllocUnit | GetDriveType | GetFileName
GetFileSize | GetFileVersion | GetFileVerStruct
GetGroup | GetLicInfoFromVBL | GetPathName
GetRemoteSupport-FileVerStruct | GetTempFilename | GetUNCShareName
GetWindowsDir | GetWindowsSysDir | GetWinPlatform
IncrementRefCount | InitDiskInfo | intGetHKEYIndex
IntGetNextFldOffset | IsDisplayNameUnique | IsNewerVer
IsSeparator | IsUNCName | IsValidDestDir
IsWin32 | IsWindows95 | IsWindowsNT
IsWindowsNT4-WithoutSP2 | KillTempFolder | LogError
LogNote | LogSilentMsg | LogSMSMsg
LogWarning | LongPath | MakeLongPath
MakePath | MakePathAux | MoveAppRemovalFiles
MsgError | MsgFunc | MsgWarning
NewAction | NTWithShell | PackVerInfo
ParseDateTime | PerformDDE | Process-CommandLine
PromptForNextDisk | ReadIniFile | ReadProtocols
ReadSetupFileLine | ReadSetupRemoteLine | RegCloseKey
RegCreateKey | RegDeleteKey | RegEdit
RegEnumKey | RegisterApp-RemovalEXE | RegisterDAO
RegisterFiles | RegisterLicense | RegisterLicenses
RegisterVBLFile | RegOpenKey | RegPathWin-CurrentVersion
RegPathWinPrograms | RegQueryNumericValue | RegQueryRefCount
RegQueryStringValue | RegSetNumericValue | RegSetStringValue
RemoteRegister | RemoveShellLink | ReplaceDouble-Quotes
ResolveDestDir | ResolveDestDirs | ResolveDir
ResolveResString | RestoreProgMan | SeparatePath-AndFileName
SetFormFont | SetMousePtr | ShowLoggingError
ShowPathDialog | SrcFileMissing | StartProcess
StrExtractFile-nameArg | strExtractFilenameItem | strGetCommon-FilesPath
StrGetDAOPath | strGetDriveFromPath | strGetHKEYString
StrGetPredefined-HKEYString | strGetProgramsFilesPath | StringFromBuffer
StripTerminator | strQuoteString | strRootDrive
StrUnQuoteString | SyncShell | TreatAsWin95
UpdateStatus | WriteAccess | WriteMIF

Calling on All Your Problem-Solving Skills

Constantly examine your approach to solving problems, and always encourage input and criticism from all quarters on the same. Think problems through. And always profile your code!

A truly useful code profiler would include some way to time Visual Basic's routines. For example, how fast is Val when compared with its near functional equivalent CInt? You can do some of this profiling using the subclassing technique discussed in Chapter 1 (replacing some VBA routine with one of your own—see Tip 11), but here's a small example anyway:

Declarations Section

Option Explicit

Declare Function WinQueryPerformanceCounter Lib "kernel32"  _
Alias "QueryPerformanceCounter" (lpPerformanceCount As LARGE_INTEGER) _
As Long

Declare Function WinQueryPerformanceFrequency Lib "kernel32" _
Alias "QueryPerformanceFrequency" (lpFrequency As LARGE_INTEGER) _
As Long

Type LARGE_INTEGER
    LowPart     As Long
    HighPart    As Long
End Type

In a Module

Function TimeGetTime() As Single

    Static Frequency     As Long
    Dim CurrentTime      As LARGE_INTEGER

    If 0 = Frequency Then

        Call WinQueryPerformanceFrequency(CurrentTime)

        Frequency = CurrentTime.LowPart / 1000

        TimeGetTime = 0

    Else

        Call WinQueryPerformanceCounter(CurrentTime)

        TimeGetTime = CurrentTime.LowPart / Frequency

    End If

End Function

Replacement for Val

Public Function Val(ByVal exp As Variant) As Long

    Dim l1 As Single, l2 As Single

    l1 = TimeGetTime()

    Val = VBA.Conversion.Val(exp)

    l2 = TimeGetTime()

    Debug.Print "Val - " & l2 - l1

End Function

The TimeGetTime routine uses the high-resolution timer in the operating system to determine how many ticks it (the operating system's precision timing mechanism) is capable of per second (WinQueryPerformanceFrequency). TimeGetTime then divides this figure by 1000 to determine the number of ticks per millisecond. It stores this value in a static variable so that the value is calculated only once.

On subsequent calls, the routine simply returns a number of milliseconds; that is, it queries the system time, converts that to milliseconds, and returns this value. For the calling program to determine a quantity of time passing, it must call the routine twice and compare the results of two calls. Subtract the result of the second call from the first, and you'll get the number of milliseconds that have elapsed between the calls. This process is shown in the "Replacement for Val" code.

With this example, one can imagine being able to profile the whole of VBA. Unfortunately, that isn't possible. If you attempt to replace certain routines, you'll find that you can't. For example, the CInt routine cannot be replaced using this technique. (Your replacement CInt is reported as being an illegal name.) According to Microsoft, for speed, some routines were not implemented externally in the VBA ActiveX server but were kept internal—CInt is one of those routines.

Using Smoke and Mirrors

The best optimization is the perceived one. If you make something look or feel fast, it will generally be perceived as being fast. Give your users good feedback. For example, use a progress bar. Your code will actually run slower (it's having to recalculate and redraw the progress bar), but the user's perception of its speed, compared to not having the progress bar, will almost always be in your favor.

One of the smartest moves you can ever make is to start fast. (Compiling to native code creates "faster to start" executables.) Go to great lengths to get that first window on the screen so that your users can start using the application. Leave the logging onto the database and other such tasks until after this first window is up. Look at the best applications around: they all start, or appear to start, very quickly. If you create your applications to work the same way, the user's perception will be "Wow! This thing is usable and quick!" Bear in mind that lots of disk activity before your first window appears means you're slow: lots after, however, means you're busy doing smart stuff!

Because you cannot easily build multithreaded Visual Basic applications (see Chapter 13 to see some light at the end of this particular tunnel), you might say that you'll have to block sometime; that is, you're going to have to log on sometime, and you know that takes time—and the user will effectively be blocked by the action. Consider putting the logging on in a separate application implemented as an out-of-process ActiveX server, perhaps writing this server to provide your application with a set of data services. Use an asynchronous callback object to signal to the user interface part of your application when the database is ready to be used. When you get the signal, enable those features that have now become usable. If you take this approach, you'll find, of course, that the data services ActiveX server is blocked—waiting for the connection—but your thread of execution, in the user interface part of the application, is unaffected, giving your user truly smooth multitasking. The total effort is minimal; in fact, you might even get some code reuse out of the ActiveX server. The effect on the user's perception, however, can be quite dramatic.

As I've said before, compiled code is faster than p-code, so of course, one "easy" optimization everyone will expect to make is to compile to native code. Surely this will create faster-executing applications when compared to a p-code clone?

Using the TimeGetTime routine, we do indeed see some impressive improvements when we compare one against the other. For example the following loop code, on my machine (300 MHz, Pentium Pro II, 128 MB RAM), takes 13.5 milliseconds to execute as compiled p-code and just 1.15 milliseconds as native code—almost 12 times faster (optimizing for fast code and the Pentium Pro). If this kind of improvement is typical, "real" compilation is, indeed, an easy optimization.

Dim n As Integer
Dim d As Double

For n = 1 To 32766
    ' Do enough to confuse the optimizer.
    d = (n * 1.1) - (n * 1#)
Next


Stuff About Objects, Types, and Data Structures

Code reuse is mostly about object orientation—the effective packaging of components to suit some plan or design. This section examines the mechanisms that exist in Visual Basic to effectively bring about code reuse. In particular, we'll look at how we can extend the type system in Visual Basic.

Visual Basic as an Object-Oriented Language

People often say that Visual Basic is not properly object oriented. I would answer that if you were comparing it with C++, you're both right and wrong. Yes, it isn't C++; it's Visual Basic!

C++ is a generic, cross-platform programming language designed around a particular programming paradigm that is, subjectively, object oriented. It is based on and influenced by other programming languages such as Simula, C, and C with Objects2.

Visual Basic has evolved—and has been influenced, too—according to system considerations, not primarily to different languages. It was designed to be platform specific; there's not an implementation for the VAX computer, for example. Visual Basic's object orientation, then, is not primarily language based. Its object-oriented language constructs are not there to implement object orientation directly but rather to best utilize the object-oriented features of the operating system—in Windows, of course, this means ActiveX.

ActiveX itself, however, is not a language definition but a systems-level object technology built directly into a specific range of operating systems. It is not subject to committees either, although you might consider this to be a negative point. Additionally, I think I'd call ActiveX and Visual Basic "commercial," whereas I'd probably call C++ "academic." I have nothing against C++ or any other programming language. Indeed, I'm a proponent of mixed-language programming and use C++ almost daily. What I am against, however, is a comparison of Visual Basic with C++. These two languages are as different as COBOL and FORTRAN, and both were designed to solve different problems and to cater to different markets. This all said, I'm still keen to model the world realistically in terms of objects, and I also want to encourage both high cohesion and loose coupling between and within those objects. (Here's a quick tip for the latter: Use ByVal—it helps!) Whether or not I achieve this cohesion and coupling with Visual Basic and ActiveX is the real question.

Cohesion and coupling

cohesion and coupling. A component is said to be cohesive if it exhibits a high degree of functional relatedness with other related components. These related components (routines typically) should form cohesive program units (modules and classes). Every routine in a module should, for example, be essential for that module to accomplish its purpose. Generally, there are seven recognized levels of cohesion (none of which I'll cover here). Coupling is an indication of the strength of the interconnections and interactions exhibited by different program components. If components are strongly coupled, they obviously have a dependency on each other—neither can typically work without the other, and if you break one of the components, you'll invariably break the others that are dependent upon it. In Visual Basic, tight coupling typically comes about through the overuse and sharing of public symbols (variables, constants, properties, and routines exported by other units).

What are your intentions?

Having an object implies intention; that is, you're about to do something with the object. This intention should, in turn, define the object's behavior and its interfaces. Indeed, a strong type system implies that you know what you'll do with an object when you acquire one. After all, you know what you can do with a hammer when you pick one up! A sense of encapsulation, identity, and meaning is an obvious requirement. To add both external and procedural meaning to an object, you need to be able to add desirable qualities such as methods and properties. What does all this boil down to in Visual Basic? The class, the interface, and the object variable.

Classes are essentially Visual Basic's way of wrapping both method, which is ideally the interface, and state—that is, providing real type extensions (or as good as it gets currently). A real type is more than a description of mere data (state); it also describes the set of operations that can be applied to that state (method). Unfortunately, methods are currently nonsymbolic. One feature that C++ has that I'd love to have in Visual Basic is the ability to define symbolic methods that relate directly to a set of operators. With this ability, I could, for example, define what it means to literally add a deposit to an account using the addition operator (+). After all, the plus sign is already overloaded (defined for) in Visual Basic. For example the String type supports addition. Also, a solution will have nothing to do with ActiveX; the ability to define symbolic methods is a mere language feature.

Visual Basic "inheritance"

Visual Basic lacks an inheritance mechanism (definitely more of an ActiveX constraint) that is comparable with that of C++. To reuse another object's properties in Visual Basic, you must use something else—either composition or association (composition meaning aggregation—like in a UDT; association meaning, in Visual Basic-speak, an object reference). Historically, association is "late" composition—as a result it also invalidates strong typing. A Visual Basic object cannot, in the C++ sense, be a superclass of another type. In other words, you cannot describe a PC, say, as being either a specialization of or composed of a CPU.

NOTE
By the way, C++ type inheritance is often used badly; that is, an object that inherits from other objects exports their public interfaces. The result is that top-level objects are often bloated because they are the sum of all the public interfaces of the objects they are derived from—a sort of overprivileged and overfed upper class!

Components that are highly cohesive, yet loosely coupled, are more easily shared—if code reuse is an issue, consider rigorously promoting both of these simple philosophies.

Object polymorphism

Polymorphism is a characteristic by which an object is able to respond to stimuli in accordance with its underlying object type rather than in accordance with the type of reference used to apply the stimulus. The Implements statement, discussed in the next section, is Visual Basic's way of extending polymorphic types beyond that allowed by the Object type. Polymorphic types might be assigned (or "set") to point to and use one another, and they're useful when you know that one type has the same interface as another type (one you'd like to treat it as). The really important thing is that with polymorphism, an object responds according to its type rather than the type of reference you have to it.

Let me give you an example using two constructs that are probably familiar to you, the Object type and a window handle.

What is the result of this code?

Dim o As Object

Set o = frmMainForm

MsgBox o.Caption

You can see here that the Caption property evaluation is applied to what the pointer/object reference o points to rather than according to what Object.Caption means. That is, that object to which we bind the Caption access to is decided in the context of what o is set to when o.Caption is executed. (We call this behavior late binding, by the way.) Notice that the code doesn't error with a message that says, "The Object class doesn't support this property or method." Polymorphism says that an object responds as the type of object it is rather than according to the type of the reference we have to it. Again, notice that I didn't have to cast the reference, (using a fabricated cast operator that you might take at first glance to be an array index), like this one, for instance:

CForm(o).Caption

The object o knows what it is (currently) and responds accordingly. Obviously, we can alter what o points to:

Dim o As Object
If blnUseForm = True Then
    Set o = frmMainForm
Else
    Set o = cmdMainButton
End If
MsgBox o.Caption

Again, o.Caption works in either case because of polymorphism.

The second example is a window handle. This window handle is something like the object reference we used above, meaning it's basically a pointer that's bound to an object at run time. The object, of course, is a Windows' window—a data structure maintained by the operating system, an abstract type. You can treat an hWnd in a more consistent fashion than you can something declared As Object, however. Basically you can apply any method call to hWnd and it'll be safe. You're right in thinking that windows are sent messages, but the message value denotes some action in the underlying hWnd. Therefore, we can think of a call to SendMessage not as sending a window a message but rather as invoking some method, meaning that we can treat SendMessage(Me.hWnd, WM_NULL, 0, 0) as something like hWnd.WM_NULL 0, 0. The WM_NULL (a message you're not meant to respond to) is the method name, and the 0, 0 are the method's parameters. All totally polymorphic—an hWnd value is a particular window and it will respond accordingly.

Another similarity between the window handle and the Object type is that a window is truly an abstract type (meaning that you cannot create one without being specific), and so is the Object type. You can declare something As Object— though now it seems what I've just said is not true—but what you've done, in fact, is not create an Object instance but an instance of a pointer to any specific object (an uninitialized object reference). It's like defining a void pointer in C. The pointer has potential to point somewhere but has no implementation and therefore no sense in itself. It's only meaningful when it's actually pointing to something!

I hope you can see that polymorphism is great for extending the type system and for being able to treat objects generically while having them respond according to what they actually are. Treating objects as generically as possible is good; specialize only when you really need to.

OK, so what's the catch? Well, the problem with As-Object polymorphism is that it isn't typesafe. What would happen in my earlier example if, once I'd set o to point to the cmdMainButton command button, I tried to access the WindowState property instead of the Caption property? We'd get an error, obviously, and that's bad news all around. What we really need is a more typesafe version of Object, with which we can be more certain about what we might find at the other end of an object reference (but at the same time keeping the polymorphic behavior that we all want). Enter Implements.

Using Implements

The Implements statement provides a form of interface inheritance. A class that uses the Implements statement inherits the interface that follows the Implements keyword but not, by some magic, an implementation of that interface. A class is free to define code for the inherited interface methods, or the class can choose to leave the interface methods as blank stubs. On the other side of the Implements equation, we can define an interface that other classes can inherit but have nothing to do with a particular implementation.

When you inherit an interface by using the Implements statement, you're providing methods and properties with certain names on an object. Now, when your containing class is initialized, it should create a suitable implementation of these promised methods and properties. This can happen in one of two ways:

  1. Pass method and property invocations on to the underlying implementation (by creating and maintaining a local copy of an object that actually implements the methods and properties). This mechanism is often called forwarding.

  2. Handle the invocation entirely by itself.

How does this differ from As-Object polymorphism? Basically, when you set a typed object reference to point to an object, the object instance to which you set it must implement the interfaces specified by the type of the object reference you use in the declaration. This adds an element of type safety to the dynamic typecast, which is what you're implicitly doing, of course.

When you choose not to implement an interface other than by forwarding requests to the underlying base type (the thing you've said you implement) you can get these problems:

  1. The derived class now acts like a base class—you've just provided a pass-through mechanism.

  2. The base object reference is made public in the derived class (by accident), and because you cannot declare Const references, the reference to your implementation might be reassigned. (Actually, it's just as easy to do this via composition in Visual Basic.)

A consequence of the second problem is that you can accidentally "negate" the reference to the base object. Say, for argument's sake, that you set it to point to a Form; clearly the derived class no longer implements a base but a Form. Better hope your method names are different in a base and a Form or you're in for a surprise!

It's important to realize that Implements can be used with forwarding or via composition. The only difference in Visual Basic is the keyword New—in fact, it's even grayer than that. In class Derived, does the following code mean we're using forwarding or composition?

Implements Base

Private o As New Base

Do we contain a Base object or do we simply have a variable reference to a Base instance? Yes, it's the latter. In Visual Basic you cannot compose an object from others because you cannot really define an object—only an object reference.

Let me summarize this statement and the meaning of Implements. When you're given an object reference you have to consider two types: the type of the reference and the type of the object referenced (the "reference" and the "referent"). Implements ensures that the type of the referent must be "at least as derived as" the type of the reference. In other words, if a Set works, you have at least a reference type referent object attached to the reference. This leads us to the following guarantee: If the class of the reference has the indicated method or property (Reference_Type.Reference_Method), then the object reference—the referent—will have it, too.

Delegating objects

Delegating objects consists of two parts. Part one deals with who responds—through my interface I might get an actual implementor of the interface (the object type I have said that I implement) to respond (possibly before I respond), or I might elect to generate the whole response entirely by myself. Part two deals with who is responsible for an object; this is used in conjunction with association. Two containers might deal with a single provider object at various times. This use of association raises the question of object ownership (which container should clean up the object, reinitialize and repair it, and so forth).

Object orientation is modeling the requirements. Defining the requirements therefore dictates the object model and implementation method you'll employ. You can build effective sets of objects in Visual Basic, but you cannot do today all of what is possible in C++. As I said earlier, Visual Basic and C++ are two different languages, and you should learn to adapt to and utilize the strengths of each as appropriate.

Using Collections to Extend the Type System

You can also extend the type system ("type" meaning mere data at this point). At TMS, we often use a Collection object to represent objects that are entirely entities of state; that is, they have no methods. (You cannot append a method to a collection.) See Chapter 1 for more on building type-safe versions of Visual Basic's intrinsic types (called Smarties).

Dim KindaForm As New Collection

Const pHeight As String = "1"
Const pWidth  As String = "2"
Const pName   As String = "3"
' ...

With KindaForm
    .Add Key:=pHeight, Item:=Me.Height
    .Add Key:=pWidth, Item:=Me.Width
    .Add Key:=pName, Item:=Me.Name
End With

' ...

With KindaForm
    Print .Item(pHeight)
    Print .Item(pWidth)
    Print .Item(pName)
End With

Here we have an object named KindaForm that has the "properties" pHeight, pWidth, and pName. In other words, an existing Visual Basic type (with both properties and method) is being used to create a generic state-only object. If you're using classes to do this, you might want to consider using Collection objects as shown here instead.

You can add functional members to a Collection object with just one level of indirection by adding an object variable to the collection that is set to point to an object that has the necessary functionality defined in it. Such methods can act on the state in the other members of the collection.

So what's the difference between using a collection and creating a user-defined type (UDT)? Well, a collection is more flexible (not always an advantage) and has support for constructs such as For Each:

For Each v In KindaForm
    Print v
Next

The advantage of UDTs is that they have a known mapping. For example, they can be used as parameters to APIs, sent around a network and passed between mainframe and PC systems—they are just byte arrays. (See Chapter 4 for more on UDTs—they're one of Jon Burns's favorite things!) Obviously, a state-only Collection object doesn't mean much to a mainframe system, and passing KindaForm as "the thing" itself will result in your only passing an object pointer to a system that cannot interpret it. (Even if it could, the object would not be available because it's not transmitted with its address.)

Adding to VarType

Another "byte array" way to extend the type system is to add in new Variant types. In Visual Basic 5, the following subtypes were available via the Variant:
Visual Basic Name | VarType | Description
vbEmpty | 0 | Uninitialized (default)
vbNull | 1 | Contains no valid data
vbInteger | 2 | Integer
vbLong | 3 | Long integer
vbSingle | 4 | Single-precision floating-point number
vbDouble | 5 | Double-precision floating-point number
vbCurrency | 6 | Currency
vbDate | 7 | Date
vbString | 8 | String
vbObject | 9 | Automation object
vbError | 10 | Error
vbBoolean | 11 | Boolean
vbVariant | 12 | Variant (used only for arrays of Variants)
vbDataObject | 13 | Data access object
vbDecimal | 14 | Decimal
vbByte | 17 | Byte
vbArray | 8192 | Array

In Visual Basic 6, we have a new addition (and a great deal of scope for adding more—a lot of gaps!):

   vbUserDefinedType	36	User-defined type

With some limitations, we can add to this list. For example, we could, with only a small amount of effort, add a new Variant subtype of 42 to represent some new entity by compiling this C code to a DLL named NEWTYPE.DLL:

#include "windows.h"
#include "ole2.h"
#include "oleauto.h"

#include <time.h>

typedef VARIANT * PVARIANT;

VARIANT  __stdcall CVNewType(PVARIANT v)
{
    // If the passed Variant is not set yet...
    if (0 == v->vt)
    {
        // Create new type.
        v->vt = 42;

        // Set other Variant members to be meaningful
        // for this new type...

        // You do this here!
    }

    // Return the Variant, initialized/used Variants
    // unaffected by this routine.
    return *v;
}

int  __stdcall EX_CInt(PVARIANT v)
{
    // Sanity check - convert only new Variant types!
    if (42 != v->vt)
    {
        return 0;
    }
    else
    {
        // Integer conversion - get our data and convert it as
        // necessary.
        // Return just a random value in this example.
        srand((unsigned)time(NULL));

        return rand();
    }
}

This code provides us with two routines: CVNewType creates, given an already created but empty Variant (it was easier), a Variant of subtype 42; EX_CInt converts a Variant of subtype 42 into an integer value (but doesn't convert the Variant to a new Variant type). "Converts" here means "evaluates" or "yields". Obviously, the implementation above is minimal. We're not putting any real value into this new Variant type, and when we convert one all we're doing is returning a random integer. Nevertheless, it is possible! Here's some code to test the theory:

Dim v As Variant

v = CVNewType(v)

Me.Print VarType(v)
Me.Print EX_CInt(v)

This code will output 42 and then some random number when executed against the DLL. The necessary DLL declarations are as follows:

Private Declare Function CVNewType Lib "NEWTYPE.DLL" _
    (ByRef v As Variant) As Variant
Private Declare Function EX_CInt   Lib "NEWTYPE.DLL" _
    (ByRef v As Variant) As Integer

Again, we cannot override Visual Basic's CInt , and so I've had to call my routine something other than what I wanted to in this case, EX_CInt for "external" CInt. I could, of course, have overloaded Val:

Public Function Val(ByRef exp As Variant) As Variant

    Select Case VarType(exp)

        Case 42:   Val = EX_CInt(exp)
        Case Else: Val = VBA.Conversion.Val(exp)

    End Select

End Function

Here, if the passed Variant is of subtype 42, I know that the "real" Val won't be able to convert it—it doesn't know what it holds after all—so I convert it myself using EX_CInt. If, however, it contains an old Variant subtype, I simply pass it on to VBA to convert using the real Val routine.

Visual Basic has also been built, starting with version 4, to expect the sudden arrival of Variant subtypes about which nothing is known. This assertion must be true because Visual Basic 4 can be used to build ActiveX servers that have methods. In turn, these can be passed Variants as parameters. A Visual Basic 5 client (or server) can be utilized by a Visual Basic 6 client! In other words, because a Visual Basic 6 executable can pass in a Variant of subtype 14, Visual Basic must be built to expect unknown Variant types, given that the number of Variant subtypes is likely to grow at every release. You might want to consider testing for this in your Visual Basic 4 code.

Having said all this and having explained how it could work, I'm not sure of the real value currently of creating a new Variant subtype. This is especially true when, through what we must call a feature of Visual Basic, not all the conversion routines are available for subclassing. Why not use a UDT, or better still, a class to hold your new type instead of extending the Variant system?

Another limitation to creating a new Variant subtype is because of the way we cannot override operators or define them for our new types. We have to be careful that, unlike an old Variant, our new Variant is not used in certain expressions. For example, consider what might happen if we executed Me.Print 10 + v. Because v is a Variant, it needs to be converted to a numeric type to be added to the integer constant 10. When this happens, Visual Basic must logically apply VarType to v to see what internal routine it should call to convert it to a numeric value. Obviously, it's not going to like our new Variant subtype! To write expressions such as this, we'd need to do something like Me.Print 10 + Val(v). This is also the reason why, in the Val substitute earlier, I had to pass exp by reference. I couldn't let Visual Basic evaluate it, even though it's received as a Variant.

Variants also might need destructing correctly. When they go out of scope and are destroyed, you might have to tidy up any memory they might have previously allocated. If what they represent is, say, a more complex type, we might have to allocate memory to hold the representation.

Microsoft does not encourage extending the Variant type scheme. For example, 42 might be free today, but who knows what it might represent in Visual Basic 7. We would need to bear this in mind whenever we created new Variant subtypes and make sure that we could change their VarType values almost arbitrarily—added complexity that is, again, less than optimal!

All in all, creating new Variant subtypes is not really a solution at the moment. If we get operator overloading and proper access to VBA's conversion routines, however, all of this is a little more attractive.

NOTE
The code to create Variant subtypes needs to be written in a language such as C. The main reason is that Visual Basic is too type safe and simply won't allow us to treat a Variant like we're doing in the DLL. In other words, accessing a Variant in Visual Basic accesses the subtype's value and storage transparently through the VARIANT structure. To access its internals, it's necessary to change the meaning of Variant access from one of value to one of representation.

Pointers

A common criticism of Visual Basic is that it doesn't have a pointer type. It cannot therefore be used for modeling elaborate data types such as linked lists. Well, of course, Visual Basic has pointers—an object variable can be treated as a pointer. Just as you can have linked lists in C, so you can have them in Visual Basic.

Creating a linked list

Let's look at an example of a circular doubly linked list where each node has a pointer to the previous and next elements in the list, as shown in Figure 7-2. Notice in the code that we have a "notional" starting point, pHead, which initially points to the head of the list.

Figure 7-2 A node in the list

The Node Class

Option Explicit

' "Pointers" to previous and next nodes.
Public pNext As Node
Public pPrev As Node

' Something interesting in each node -
' the creation number (of the node)!
Public nAttribute As Integer

Private Sub Class_Initialize()

    Set pNext = Nothing
    Set pPrev = Nothing

End Sub

Private Sub Class_Terminate()

    ' When an object terminates, it will already have
    ' had to set these two members to Nothing:
    ' this code, then, is slightly redundant.
    Set pNext = Nothing
    Set pPrev = Nothing

End Sub

The Test Form

Option Explicit

Private pHead   As New Node
Private pV      As Node

Public Sub CreateCircularLinkedList()

    Dim p           As Node
    Dim nLoop       As Integer
    Static pLast    As Node    ' Points to last node created
                               ' pHead if first node.

    pHead.nAttribute = 0

    Set pLast = pHead

    ' 501 objects in list - the pHead object exists
    ' until killed in DeleteList.

    For nLoop = 1 To 501

        Set p = New Node

        p.nAttribute = nLoop

        Set pLast.pNext = p
        Set p.pPrev = pLast

        Set pLast = p

    Next

    ' Decrement reference count on object.
    Set pLast = Nothing

    ' Join the two ends of the list, making a circle.
    Set p.pNext = pHead
    Set pHead.pPrev = p

    Exit Sub

End Sub

Public Sub PrintList()

    Debug.Print "Forwards"

    Set pV = pHead

    Do
        Debug.Print pV.nAttribute

        Set pV = pV.pNext

    Loop While Not pV Is pHead

    Debug.Print "Backwards"

    Set pV = pHead.pPrev

    Do
        Debug.Print pV.nAttribute

        Set pV = pV.pPrev

    Loop While Not pV Is pHead.pPrev

End Sub

Public Sub DeleteList()

    Dim p As Node

    Set pV = pHead

    Do
        Set pV = pV.pNext
        Set p = pV.pPrev

        If Not p Is Nothing Then
            Set p.pNext = Nothing
            Set p.pPrev = Nothing
        End If

        Set p = Nothing

    Loop While Not pV.pNext Is Nothing

    ' Both of these point to pHead at the end.
    Set pV = Nothing
    Set pHead = Nothing

End Sub

The routines CreateCircularLinkedList, PrintList, and DeleteList should be called in that order. I have omitted building in any protection against deleting an empty list. To keep the example as short as possible, I've also excluded some other obvious routines, such as -InsertIntoList.

In Visual Basic, a node will continue to exist as long as an object variable is pointing to it (because a set object variable becomes the thing that the node is set to). For example, if two object variables point to the same thing, an equality check of one against the other (using Is) will evaluate to True (an equivalence operator). It follows, then, that for a given object all object variables that are set to point to it have to be set to Nothing for it to be destroyed. Also, even though a node is deleted, if the deleted node had valid pointers to other nodes, it might continue to allow other nodes to exist. In other words, setting a node pointer, p, to Nothing has no effect on the thing pointed to by p if another object variable, say, p1, is also pointing to the thing that p is pointing to. This means that to delete a node we have to set the following to Nothing: its pPrev object's pNext pointer, its pNext object's pPrev pointer, and its own pNext and pPrev pointers (to allow other nodes to be deleted later). And don't forget the object variable we have pointing to p to access all the other pointers and objects. Not what you might expect!

It's obvious that an object variable can be thought of as a pointer to something and also as the thing to which it points. Remember that Is should be used to compare references, not =. This is why we need Set to have the variable point to something else; that is, trying to change the object variable using assignment semantically means changing the value of the thing to which it points, whereas Set means changing the object variable to point elsewhere. In fact nearly any evaluation of an object variable yields the thing to which the object variable is pointing to. An exception is when an object variable is passed to a routine as a parameter, in which case the pointer is passed, not the value (the object) that it's pointing to. (The object also has an AddRef applied to it.)

Linked lists that are created using objects appear to be very efficient. They are fast to create and manipulate and are as flexible as anything that can be created in C.

Visual Basic 6 (VBA) is also able to yield real pointers, or addresses. Three undocumented VBA methods—VarPtr, ObjPtr, and StrPtr (which are just three different VBA type library aliases pointing to the same entry point in the run-time DLL)—are used to create these pointers. You can turn an object into a pointer value using l = ObjPtr(o), where o is the object whose address you want and l is a long integer in which the address of the object is put. Just resolving an object's address doesn't AddRef the object, however. You can pass this value around and get back to the object by memory copying l into a dummy object variable and then setting another object variable to this dummy (thus adding a reference to the underlying object).

Call CopyMemory(oDummy, l, 4)
Set oThing = oDummy

CopyMemory should be defined like this:

Private Declare Sub CopyMemory Lib "kernel32" _
    Alias "RtlMoveMemory" (pDest As Any, pSource As Any, _
    ByVal ByteLen As Long)

The really neat thing here is that setting l doesn't add a reference to the object referenced by the argument of ObjPtr. Normally, when you set an object variable to point to an object, the object to which you point it (attach it, really) has its reference count incremented, meaning that the object can't be destroyed, because there are now two references to it. (This incrementing also happens if you pass the object as a parameter to a routine.) For an example of how this can hinder your cleanup of objects, see the discussion of the linked list example.

By using VarPtr (which yields the address of variables and UDTs), StrPtr (which yields the address of strings), and ObjPtr, you can create very real and very powerful and complex data structures.

Here's the short piece of code I used to discover that VarPtr, ObjPtr, and StrPtr are all pretty much the same thing (that is, the same function in a DLL):

Private Sub Form_Load()

    ' VB code to dump or match an external
    ' server method with a DLL entry point. Here it's
    ' used to dump the methods of the "_HiddenModule".

    ' Add a reference to 'TypeLib Information' (TLBINF32.DLL),
    ' which gives you TLI before running this code.

    Dim tTLInfo  As TypeLibInfo
    Dim tMemInfo As MemberInfo
    Dim sDLL     As String
    Dim sOrdinal As Integer

    Set tTLInfo = _
        TLI.TLIApplication.TypeLibInfoFromFile("MSVBVM50.DLL")

    For Each tMemInfo In _
        tTLInfo.TypeInfos.NamedItem("_HiddenModule").Members

        With tMemInfo
            tMemInfo.GetDllEntry sDLL, "", sOrdinal

            ' labDump is the label on the form where the
            ' output will be printed.
            labDump.Caption = labDump.Caption & _
                    .Name & _
                    " is in " & _
                    sDLL & _
                    " at ordinal reference " & sOrdinal & _
                    vbCrLf
        End With

    Next

End Sub

The code uses TLBINF32.DLL, which can interrogate type libraries (very handy). Here I'm dumping some information on all the methods of a module (in type library parlance) named _HiddenModule. You'll see that this is the module that contains VarPtr, ObjPtr, and StrPtr, which you can discover using OLEVIEW.EXE to view MSVBVM60.DLL:

module _HiddenModule {
        [entry(0x60000000), vararg, helpcontext(0x000f6c9d)]
        VARIANT _stdcall Array([in] SAFEARRAY(VARIANT)* ArgList);
        [entry(0x60000001), helpcontext(0x000f735f)]
        BSTR _stdcall _B_str_InputB(
                        [in] long Number,
                        [in] short FileNumber);
        [entry(0x60000002), helpcontext(0x000f735f)]
        VARIANT _stdcall _B_var_InputB(
                        [in] long Number,
                        [in] short FileNumber);
        [entry(0x60000003), helpcontext(0x000f735f)]
        BSTR _stdcall _B_str_Input(
                        [in] long Number,
                        [in] short FileNumber);
        [entry(0x60000004), helpcontext(0x000f735f)]
        VARIANT _stdcall _B_var_Input(
                        [in] long Number,
                        [in] short FileNumber);
        [entry(0x60000005), helpcontext(0x000f65a4)]
        void _stdcall Width(
                        [in] short FileNumber,
                        [in] short Width);
        [entry(0x60000006), hidden]
        long _stdcall VarPtr([in] void* Ptr);
        [entry(0x60000007), hidden]
        long _stdcall StrPtr([in] BSTR Ptr);
        [entry(0x60000008), hidden]
        long _stdcall ObjPtr([in] IUnknown* Ptr);
    };

When you run the Visual Basic code, you'll see this output:

Label1Array   is in VBA5.DLL at ordinal reference 601
_B_str_InputB is in VBA5.DLL at ordinal reference 566
_B_var_InputB is in VBA5.DLL at ordinal reference 567
_B_str_Input  is in VBA5.DLL at ordinal reference 620
_B_var_Input  is in VBA5.DLL at ordinal reference 621
Width         is in VBA5.DLL at ordinal reference 565
VarPtr        is in VBA5.DLL at ordinal reference 644
StrPtr        is in VBA5.DLL at ordinal reference 644
ObjPtr        is in VBA5.DLL at ordinal reference 644

This output shows the method name together with the DLL and ordinal reference (into the DLL) that implements its functionality. If you use DUMPBIN /EXPORTS on MSVBVM60.DLL like this:

dumpbin /exports msvbvm60.dll > dump

and then examine the dump file, you'll see that the routine at ordinal 644 is in fact VarPtr. In other words, VarPtr, ObjPtr, and StrPtr all do their stuff in the MSVBVM60.DLL routine VarPtr!

Matching the code output to the dump, we see this:

Method Name     DLL Routine Name
Label1Array     rtcArray
_B_str_InputB   rtcInputCount
_B_var_InputB   rtcInputCountVar
_B_str_Input    rtcInputCharCount
_B_var_Input    rtcInputCharCountVar
Width           rtcFileWidth
VarPtr          VarPtr
StrPtr          VarPtr
ObjPtr          VarPtr

I haven't explained what the other routines do—you can discover that for yourself.


Stuff About Type Libraries

In this section, we'll take a quick look at type libraries—not those created by Visual Basic (because they're free) but those created by hand. You'll see how to use these handmade type libraries as development tools that will help you ensure that your coding standards are correctly applied.

A type library is where Visual Basic records the description of your ActiveX server's interfaces. Put another way, a type library is a file, or perhaps part of a file, that describes the type of one or more objects. (These objects don't have to be ActiveX servers.) Type libraries do not, however, store the actual objects described—they store only information about objects. (They might also contain immediate data such as constant values.) By accessing the type library, applications can check the characteristics of an object—that is, the object's exported and named interfaces.

When ActiveX objects are exported and made public in your applications, Visual Basic creates a type library for you to describe the object's interfaces. You can also create type libraries separately using the tools found on the Visual Basic 6 CD in \TOOLS\VB\UNSUPPRT\TYPLIB.

Type libraries are usually written using a language called Object Description Language (ODL) and are compiled using MKTYPLIB.EXE. A good way to learn a little more about ODL is to study existing type libraries. You can use the OLEVIEW.EXE tool mentioned earlier to disassemble type libraries from existing DLLs, ActiveX servers, and ActiveX controls for further study.

As I just said, the information described by a type library doesn't necessarily have anything to do with ActiveX. Here are a couple of handy examples to show how you might use type libraries.

Removing Declare Statements

You might have noticed that throughout this book we generally prefix Windows API calls with Win to show that the routine being called is in Windows, that it's an API call. You've also seen how to make these calls using Alias within the declaration of the routine. (Alias allows you to rename routines.) Here BringWindowToTop is being renamed WinBringWindowToTop:

Declare Function WinBringWindowToTop Lib "user32" _
    Alias "BringWindowToTop" (ByVal hwnd As Long) As Long

However, we could use a type library to do the same thing. Here's an entire type library used to do just that:

APILIB.ODL

' The machine name for a type library is a GUID.
[uuid(9ca45f20-6710-11d0-9d65-00a024154cf1)]

library APILibrary
{
    [dllname("user32.dll")]

    module APILibrary
    {
        [entry("BringWindowToTop")] long stdcall
            WinBringWindowToTop([in] long hWnd);
    };
};

MAKEFILE

apilib.tlb : apilib.odl makefile
    mktyplib /win32 apilib.odl

The MAKEFILE is used to create the TLB file given the ODL file source code. To run MAKEFILE, invoke NMAKE.EXE. If you don't have NMAKE.EXE, simply run MKTYPLIB.EXE from a command prompt like this:

mktyplib /win32 apilib.odl

The type library contains a description of an interface in APILibrary named WinBringWindowToTop. Once you have compiled the library, run Visual Basic and select References from the Project menu. Click the Browse button in the References dialog box to find the APILIB.TLB file, and then select it, as shown in Figure 7-3.

Click to view at full size.

Figure 7-3 Selecting APILibrary (APILIB.TLB) in the References dialog box

Click OK and press F2 to bring up Visual Basic's Object Browser, which is shown in Figure 7-4:

Figure 7-4 APILibrary displayed in the Object Browser

In Figure 7-4, notice that the method WinBringWindowToTop seems to be defined in a module and a server, both named APILibrary. Notice also that we have access to the syntax of the method. (The Quick Info help in Visual Basic will also display correctly for this method.) To use the method (which is really a function in USER32.DLL), all we have to do is enter code. No DLL declaration is now required (and so none can be entered incorrectly).

Call WinBringWindowToTop(frmMainForm.hWnd)

Another useful addition to a type library is named constants. Here's a modified APILIB.ODL:

[uuid(9ca45f20-6710-11d0-9d65-00a024154cf1)]

library APILibrary
{

    [dllname("user32.dll")]

    module WindowsFunctions
    {
        [entry("BringWindowToTop")] long stdcall
            WinBringWindowToTop([in] long hWnd);
        [entry("ShowWindow")] long stdcall
            WinShowWindow([in] long hwnd, [in] long nCmdShow);
    };

    typedef
    [
        uuid(010cbe00-6719-11d0-9d65-00a024154cf1),
        helpstring
        ("WinShowWindow Constants - See SDK ShowWindow for more.")
    ]
    enum
    {
        [helpstring("Hides the window, activates another")]
            SW_HIDE = 0,
        [helpstring("Maximizes the window")]
            SW_MAXIMIZE = 3,
        [helpstring("Minimizes the window activates next window")]
            SW_MINIMIZE = 6,
        [helpstring("Activates the window")]
            SW_RESTORE = 9,
        [helpstring("Activates/displays (current size and pos)" )]
            SW_SHOW = 5,
        [helpstring("Sets window state based on the SW_ flag")]
            SW_SHOWDEFAULT = 10,
        [helpstring("Activates window - displays maximized")]
            SW_SHOWMAXIMIZED = 3,
        [helpstring("Activates window - displays minimized")]
            SW_SHOWMINIMIZED = 2,
        [helpstring("Displays  window minimized")]
            SW_SHOWMINNOACTIVE = 7,
        [helpstring("Displays  window to current state.")]
            SW_SHOWNA = 8,
        [helpstring("Displays  window (current size and pos)")]
            SW_SHOWNOACTIVATE = 4,
        [helpstring("Activates and displays window")]
            SW_SHOWNORMAL = 1,
    } WinShowWindowConstants;

};

The library (APILibrary) now contains two sections, WindowsFunctions and WinShowWindowConstants, as shown in Figure 7-5.

Figure 7-5 APILibrary with named constants displayed in the Object Browser

The long numbers [uuid(9ca45f20-6710-11d0-9d65-00a024154cf1)] used in the ODL file are Globally Unique IDs (GUIDs). (See Chapter 1, for more detailed information on GUIDs.) Just for your interest, here's a small Visual Basic program that'll generate GUIDs for you. No matter how many times you run this program (which outputs a GUID for each button click), it will never produce the same GUID twice!

Declaration Section

Option Explicit

Private Type GUID
    D1       As Long
    D2       As Integer
    D3       As Integer
    D4(8)    As Byte
End Type

Private Declare Function WinCoCreateGuid Lib "OLE32.DLL" _
    Alias "CoCreateGuid" (g As GUID) As Long

CreateGUID

Public Function CreateGUID() As String

    Dim g          As GUID
    Dim sBuffer    As String

    Dim nLoop As Integer

    Call WinCoCreateGuid(g)

    sBuffer = PadRight0(sBuffer, Hex$(g.D1), 8, True)
    sBuffer = PadRight0(sBuffer, Hex$(g.D2), 4, True)
    sBuffer = PadRight0(sBuffer, Hex$(g.D3), 4, True)
    sBuffer = PadRight0(sBuffer, Hex$(g.D4(0)), 2)
    sBuffer = PadRight0(sBuffer, Hex$(g.D4(1)), 2, True)
    sBuffer = PadRight0(sBuffer, Hex$(g.D4(2)), 2)
    sBuffer = PadRight0(sBuffer, Hex$(g.D4(3)), 2)
    sBuffer = PadRight0(sBuffer, Hex$(g.D4(4)), 2)
    sBuffer = PadRight0(sBuffer, Hex$(g.D4(5)), 2)
    sBuffer = PadRight0(sBuffer, Hex$(g.D4(6)), 2)
    sBuffer = PadRight0(sBuffer, Hex$(g.D4(7)), 2)

    CreateGUID = sBuffer

End Function

PadRight0

Public Function PadRight0( _
                          ByVal sBuffer As String _
                         ,ByVal sBit As String _
                         ,ByVal nLenRequired As Integer _
                         ,Optional bHyp As Boolean _
                         ) As String

    PadRight0 = sBuffer & _
                sBit & _
                String$(Abs(nLenRequired - Len(sBit)), "0") & _
                IIf(bHyp = True, "-", "")

End Function

Command1 Click Event Handler

Private Sub Command1_Click()

    Print CreateGUID

End Sub

Notice that the optional Boolean argument in PadRight0 is set to False if it is missing in Visual Basic 6 (as it was in 5); that is, it is never actually missing. (See IsMissing in the Visual Basic 6 online help.) In Visual Basic 6, an optional argument typed as anything other than Variant is never missing. An Integer is set to 0, a String to "", a Boolean to False, and so on. Bear this in mind if you really need to know whether or not the argument was passed. If you do, you'll need to use Optional Thing As Variant and IsMissing. Even in Visual Basic 4 an object is never really missing; rather, it is set to be of type vbError (as in VarType will yield 10). I've no idea what the error's value is.

In Chapter 1, I mentioned using object instances as constants and referenced this chapter for the code. Well, here it is along with some explanation.

In Visual Basic you cannot initialize a constant from a variable expression. The Help file in Visual Basic 6 says, "You can't use variables, user-defined functions, or intrinsic Visual Basic functions, such as Chr, in expressions assigned to constants." In other words, the value of the constant must be derivable by the compiler at compile time. In Chapter 1, I wanted to use a constant to hold a value returned from the Windows API, like this:

vbObjectiSingle  = WinGlobalAddAtom(CreateGUID)

I said that the object type of vbObjectiSingle was a constant Smartie type. That said, here's the code.…


Stuff About Smarties

Here's the code for this ConstiLong class (a constant intelligent Long):

Private bInit   As Boolean
Private l       As Long

Public Property Let Value(ByVal v As Variant)

    If bInit Then
        Err.Raise 17
    Else
        bInit = True
    End If

    If vbLong <> VarType(v) Then
        Err.Raise 13
    Else
        l = CLng(v)
    End If

End Property

Public Property Get Value() As Variant

    Value = l

End Property

Class ConstiLong instances are used as constant long integers. The Value property is marked as the default property for the class (in common with normal Smarties). You can see that the Property Let allows one-time-only initialization of the contained value (l). You can also see that I'm using a Variant to type check the argument in the Let (Visual Basic then insists on my using a Variant for the Get, too). You can remove these and use a real Long if you want (and I can guarantee that you'll be set from a Long).

From Chapter 1, here's how you'd set these up (all this code is in your start-up module).

' Used to extend sub-classed VarType for Smartie Types.
'
Public vbObjectiInteger As New ConstiLong
Public vbObjectiSingle  As New ConstiLong
Public vbObjectiString  As New ConstiLong
.
.
.

Sub main()

    vbObjectiInteger = WinGlobalAddAtom(CreateGUID)
    vbObjectiSingle  = WinGlobalAddAtom(CreateGUID)
    vbObjectiString  = WinGlobalAddAtom(CreateGUID)
    .
    .
    .

End Sub

Obviously, I need one of these classes for each intrinsic type from which I want to make a constant—ConstiInt, ConstiString, and so on. (If you're not sure what's going on, see my aside on Smarties in Chapter 1.)

Another thing you can use Smarties for is recording assigned values. This might sound a bit weird, but it is a useful thing to do with them. What do I mean? Consider this code:

    Dim n As New iInteger

    n = SomeFunc(SomeParam)

End Sub

Because iInteger is a Smartie, it can, of course, do all sorts of stuff when its default property Let Value gets hit—like record the value assigned from SomeFunc in the Registry. Remember code where you have stuff like this?

' Ask the user for some info...
n = MsgBox("Show tips at startup?", vbYesNo + vbQuestion, _
           "Show Tips at Startup")

' Write away to persistent store.
Call SaveSetting(... n ...)

If vbYes = n Then ...

With Smarties you can have the same thing happen with just this assignment:

' Ask the user for some info and record it away...
n = MsgBox("Show tips at startup?", vbYesNo + vbQuestion, _
           "Show Tips at Startup")

If vbYes = n Then ...

Without proper construction (a parametizable constructor procedure for classes, also known as a declarative initialization), this assignment is of little real use. For example, how does this instance of iInteger know which key it should write to in the Registry? What you really need to support this kind of thing is declarative support, something like this—Dim n As New ipInteger(kTips). The ip here means intelligent-persistent iInteger (a class that would implement iInteger); kTips is a value that is passed to the created ipInteger instance, telling it which Registry value it should be writing to. In this scenario, it would probably write to App.EXEName\Settings\kTips. Currently the only way to parameterize the construction of objects such as this is by using public variables, such as the ones shown here.

kTips = "Tips"

Dim n As New ipInteger: n = Nothing

The n = Nothing causes n's Initialize event to trigger and read the value of the public kTips, which is really nasty, prone to error, and basically awful!

Of course, this code hides the fact that the value in n is written somewhere, so you might consider this "clever code" and discard it. It's up to you—where cleverness starts and abstraction ends is somewhat subjective. For example, what does this code look like it might do?

Dim n As New iInteger

n = n

Nothing right—the code simply assigns 0 to n. Well, that's what would happen if n were an ordinary integer, but with a Smartie we cannot be certain. As you probably know, "Dim-ing" an object using New doesn't create it—the object is created at its first use. So n is actually created when we read the value of n (because the right side of the assignment is evaluated first). This statement causes the object to be created and thus causes its Initialize event to fire. Hmm, it could be doing anything in there, like reading from the Registry and setting up its own value! Would you expect to find, if the next line read MsgBox n, that n contains, say, 42? Probably not. Of course, the code might look even more obvious:

n = 0

n = Nothing

Set n = Nothing

Notice that n = Nothing is kinda odd (in more ways than one). The statement is really "the Value (default) property of n is assigned Nothing," or Call n.Value(Nothing), so the statement is perfectly legal and causes Nothing to be passed to our object as a parameter—and, of course, causes it to be created. Notice, too, that it's very different from Set n = Nothing; this statement doesn't invoke the Value property Set even though the syntax makes it appear that the default property is being accessed. To Set the Value property, of course, you need to use Set n.Value = Nothing—see the ambiguity here? (If a default were allowed for both regular assignment and Set, Visual Basic would have no way of knowing whether you wanted to set the default property or the object variable.) Actually, depending on whether n is declared Dim n As New iInteger or Dim n As iInteger (which is then set using Set n = New iInteger), even a Set n = Nothing might have no effect. A show of hands, anyone, who thinks that an object declared Dim o As New ObjectName can be set to Nothing using Set o = Nothing. That many? Think again!

You can also, of course, log more ordinary assignments using Smarties. Should you feel that some routine is periodically returning an interesting value, have the assigned-to Smartie log the assigned value to the Registry so that you can check it.


Other Stuff

In my humble opinion, the nicest place to eat in all of Seattle (perhaps even the world, excluding The Waterside Inn in Bray in England) is The Brooklyn Seafood, Steak & Oyster House, on the intersection of Second and University in downtown. This place is just dazzling, and I have spent many a happy, self-indulgent evening there immersed in its glow and hubbub. One of my favorite things to do there is to sit at the chef's bar and watch the show on the other side of the bar. If they're makin' flames, so much the better. The chef's bar has great old-fashioned high-back swivel chairs stationed all along it for the sole purpose of allowing you to watch, and, if you're not careful, be a part of the drama taking place there. In fact, it's a lot like sitting in a theater, except it's much more intimate and generally a lot more fun!

Understanding Software Development by Dining Out

You know, it's a privilege to watch these people work—in fact the whole organization at work is amazing to watch—but the chef's bar is something special. Many a time I've sat there wishing that I could get a software team to work together half as well as the chefs do, or even that I could put together applications from readily prepared components quite so easily.

You see, I reason that I should be able to do this because the two industries have some very strong similarities. Think about it. A restaurant has a development team: an architect who plans the way it's going to be (the head chef); a technical lead—cum—project manager (whoever is the main chef for the evening); and a whole bunch of developers in the guise of sous chefs. The junior developers are the general helpers and dishwashers. In the front office are customer service representatives at the reception desk and perhaps the sales reps are the waiters and waitresses—all working on commission, mostly. Of course, a management team is trying to keep up the quality, running the whole operation and trying to make a profit. (The Brooklyn has about 50 staff in all.)

Outside the organization are a whole bunch of suppliers bringing in myriad raw and cooked components ("constituents" might be a better term), all of which have well-defined interfaces and some of which must be prepared further before they can be assembled properly to form the end product. And they do it so well, too! At every lunch and dinner they take orders from customers and rapidly create meals, using what appears to be a true Rapid Application Development (RAD) approach, They also change the menu monthly, so it wouldn't be true to say, "Well, that's fine for them because they're always producing the same thing," because they're not. So, I ask myself, why can't our industry do its thing just as easily and efficiently?

On one visit I got to chatting with Tony Cunio (head chef and co-owner). I wanted him to explain to me all the hows and whys of his trade, to explain how they consistently manage to produce the goods and yet maintain such high quality. I told him I'd like to be able to do the same. It was an interesting conversation because we each had a very strong domain vocabulary that didn't necessarily "port" too well. (For me, port is a process; for Tony, it's a drink.) It was frustrating at times—you ought to try explaining object-oriented software construction to a chef sometime!

About people, Tony says to look for people with a strong team spirit and build a strong team around you. For him, staff selection is where the quality starts. He also says that the staff have to work well under pressure and constantly stepping on each other's feet, because they practice "full-contact cooking" at The Brooklyn. Another of Tony's passions, aside from food and cooking, is his commitment to a coaching style of management. Now coaching is a subject that I'm not going to get into deeply here, except by way of trying to define it for you.

Coaching is all about a supportive relationship between the coach and the player and a way of communication between the same. The coach doesn't pass on facts but instead helps the player discover the facts, from the inside and for himself or herself. Of course, the objective is to better the performance of the player. With coaching, perhaps, how this is achieved is what's different from the normal, more traditional style of management. You know the adage—teach me and I will listen, show me and I will learn, let me do and I will understand. Well, coaching is a bit like that, except we'd probably replace that last piece with something like, "Counsel me, guide me, and trust me, but overall, let me discover, for then I will understand and grow." At TMS, we try to coach instead of manage, but I'd say that we're still learning the techniques. So far, though, the indications are good.3

What else does Tony have to offer by way of advice to an industry that is still, relatively speaking, in its start-up phase? Well, Tony always makes sure that all his staff have the right tools for the job. After all, they can't give 100 percent without the right tools, so if they need a new filleting knife, they get one. Sounds reasonable, of course, but drawing the analogy back to the software industry, how many developers can say that they are provided with the right tools for the job? Not the majority, that's for sure. Tony says that everyone needs to be fast and technically good. Again, ask yourself how skillful and fast your average developer is—this comes straight back to your hiring practices. Tony also recommends giving people responsibility, accountability, and in making them "excuse-free," meaning there are no obstacles to being successful.

Tony is very clear about the procurement of constituents."[The suppliers] are here for us, we're not here for them. I expect a faultless delivery, on time and to standard." And if he has a substandard component delivered to his kitchen? "It would go straight back—no question, garbage in, garbage out." (See? Some terms span whole industries!) "I'd also have words with the supplier to ensure that it didn't ever happen again." And what if it did? "Easy. We'd never use that supplier again." How many of you have worked with, or are working with, substandard components—and, I might add, regularly paying for what are little more than bug fixes just to discover new bugs in the upgrade? When I told Tony about the way the software industry procures components that are inevitably buggy, he was simply dumbstruck!

I asked Tony how he goes about the assembly of an entity from its components. Funnily enough, Tony recommends using a methodology, a recipe, or a plan from which people learn—not too restrictive as to limit creativity and not so unencumbered that it lacks proper rigor and guidance. Could this be what we might call a coding standard? Combined with the coaching idea, this is surely a productive and enjoyable way to learn.

It seems to me that the fundamental differences between a cooking team and a software team are that

  1. We don't practice "full-contact development" <g>, and

  2. We take what we're given, accept it for what it is (most of the time, which is inappropriate and unsuitable), and nevertheless try and come up with the goods, on time and to budget! (See Chapter 17 for the hiring perspective.)

Yup, The Brooklyn has certainly taught me a thing or two about software development over the years. Talking to Tony reminded me of a saying that I first saw in Fred Brooks' Mythical Man-Month (usually called MMM), which came out in 1972. (The latest edition is ISBN 0-201-83595-9.) It's actually a quote from the menu of Antoine's Restaurant, which is at 713-717 Rue St. Louis, New Orleans, in case you're ever there.

"Faire de la bonne cuisine demande un certain temps. Si on vous fait attendre, c'est pour mieux vous servir, et vous plaire."

which means

"To cook well requires a certain amount of time. If you must wait it is only to serve you better and please you more."

Go visit The Brooklyn and Tony next time you're in Seattle, and see if you can figure out just how they "can" and we "can't." Maybe I'll see you there, or maybe you'll bump into Michael Jordan or some other famous regular. Perhaps you'll meet some notable Microsoft "foodies" there—who knows? Anyway, enjoy your meal!

To give you an idea of what's on on the menu and to round up this chapter, here's Tony's recipe for The Brooklyn's signature dish: Alder-Planked Salmon with Smoked Tomato and Butter Sauce. (By the way, the plank is made up of blocks of alderwood secured together using steel rods and nuts. A shallow depression is then formed in the block to hold the salmon during cooking.)

Northwest Alder-Planked Salmon with Smoked Tomato Butter Sauce

Ingredients:

  • Four 8 oz. salmon fillets (skinless)

  • Salt and pepper to taste

  • One seasoned alder plank (see below)

  • 8 oz. smoked tomato butter sauce (recipe follows)

  • ¼ oz. snipped chives

  • 1 oz. Brunoise Roma tomato skin

Preparation:

  • Make smoked tomato butter sauce according to recipe. Set aside in warm area.

  • Oven cook alder plank salmon fillets.

  • Serve 2 oz. smoked tomato butter with each salmon fillet.

  • Garnish sauce with snipped chives and Brunoise Roma tomato.

  • Serve!

Ingredients for Smoked Tomato Butter (yield 3 cups):

  • 1 oz. garlic, minced

  • Two smoked tomatoes (whole)

  • 1c. white wine

  • 2 tbsp. lemon juice

  • ¾ c. cream

  • 1 lb. Butter

  • ½ tsp. seasoning salt (20 parts salt to 1 part white pepper)

Preparation:

Saute garlic in oil. Deglaze pan with wine and lemon juice. Add tomatoes and reduce to almost dry (approximately two tablespoons liquid). Add cream and reduce by half. Slowly incorporate cold butter. Season and strain. Hold in warm area until ready to serve.

Plank tips

This intriguing section might inspire you to build or buy your own wood plank!

About the wood Wood, when heated, can crack, but don't be alarmed—the steel rods and nuts are designed to prevent the plank from splitting. Make sure the nuts are screwed tightly against the wood, especially when your plank is new. The wood will slowly contract as you use it and any small cracks that develop will soon disappear.

It is especially important to season the plank when it is new. Pour one to two tablespoons of olive or vegetable oil in the hollowed oval of the plank and use a paper towel to rub the entire top of the plank, until it is lightly coated. Do not put oil on the bottom of the plank. After using your plank eight to ten times it will become well seasoned and it will be necessary to season it only on occasion.

To rekindle a stronger wood flavor after repeated use, lightly sand the plank inside the oval with fine sandpaper. Once the plank has been sanded, it should be treated like a new plank and oiled before each use until it becomes well seasoned. The bottom of the plank can also be sanded if you want to lighten the darkened wood.

Baking on wood Cooking on wood is a unique, natural, and healthy way of cooking. The wood breathes, allowing most of the juices to stay in the food, resulting in added moisture and flavor. You will find that fewer additives, sauces, or liquids will be needed. We use the plank as a beautiful serving platter right from the oven.

Preheating your plank is important so that the wood will be warm enough to cook from the bottom as well as the top. Place the plank on the middle rack of a cold oven and set to bake at 350 degrees (do not use the preheat setting). By leaving it in for 10 minutes the plank will be sterilized and ready for use. Enjoy using your plank to cook fish, poultry, meat, vegetables, and even bread.

Cleaning your plank Just use warm running water and a soft-bristled brush to clean your plank. You can use a mild soap if you want. It is easiest to clean the plank within an hour or so after use. Remember, preheating the plank before use will sterilize it.

Be careful! The plank absorbs heat and will be hot when used for cooking, so please use pot holders or oven gloves when handling it. While the plank is designed for baking, it is important to observe cooking times and the recommended temperature of 380 degrees.


Chapter 8 -- Visual Basic Programmer's Guide To Successful Dating

 Chapter 8

Visual Basic Programmer's Guide to Successful Dating

How Does Y2K Affect Visual Basic?

STEVE OVERALL

Steve has played a major part over the past couple of years in The Mandelbrot Set's drive to raise awareness of the Year 2000 issue for Visual Basic developers. He has had articles published on the subject in both the Visual Basic Programmers Journal and, in Europe, the Microsoft Developer Network Journal. He lives in leafy Surrey with the mysterious "M," his record collection, and his plants. He fully intends to be in no fit state to be aware of any problems when the clocks strike midnight on December 31, 1999.

Hands up—how many of you have heard of the "Millennium Bug" or "Year 2000 Problem" or whatever else it has been called over the last few years? If any of you didn't raise your hands, you are either not open to suggestion or you are new to this planet. Welcome! We call it Earth.

Much has been written about this subject over the past few years. While there is a great wealth of information, most of it is aimed at the COBOL community, and what isn't tends to be very generic—limited to management guides and theoretical discussions. What I want to do in this chapter is simply look at the issue from a practical perspective, focusing on its particular relevance to Visual Basic. I will look at how Visual Basic stores and manipulates date information and, equally important, what its weaknesses are.

For me the issue is not so much what happens when the clocks strike midnight on a certain night in December 1999, but that many developers still do not fully understand how our language deals with this simple piece of data!


A Little About the Date Rules

 The Gregorian calendar, which is used throughout the western world, has a long and checkered past. It was first introduced in 1582 by Pope Gregory XIII, after whom it is named.

Prior to the Gregorian calendar the Julian calendar was widely used. The Julian calendar had a leap year every four years. With the actual period of our orbit around the sun being 365.24219 days, there was a slow shifting of the seasons, until by the sixteenth century events such as the autumnal equinox were occurring up to ten days earlier than they were when the Julian calendar was introduced. The Gregorian calendar changed the rule for the century years so that they would not be leap years unless they were divisible by 400.

The new calendar was adopted in Catholic countries in 1582. Ten days were dropped to bring the seasons back into line. October 4 was immediately followed by October 15, with no dates in between. The United Kingdom and its colonies, which at the time included areas of North America, made the change in 1752 with the dropping of eleven days (September 2 was immediately followed by September 14).

NOTE
Every fourth year is a leap year except those that are also divisible by 100. However, those years divisible by 400 are leap years. So the year 2000 is a leap year; 1900 and 2100 are not.


So How Does Visual Basic Help My Dating Success?

 Here are some ways Visual Basic helps you get around the Year 2000 glitch.

The Date Data Type

Visual Basic has had a dedicated Date data type since version 4 and, prior to that (in versions 2 and 3) a Date Variant type with the same storage pattern. Dates can be declared and used like this:

Dim dteMyDate     As Date

dteMyDate = DateSerial(1998, 2, 12)

Or perhaps

dteMyDate = #2/12/98#

The Date data type is actually stored as an IEEE double-precision floating point value, 8 bytes long. The data stored can represent dates from January 1 100 up to December 31 9999. Days are stored as whole numbers, with zero being December 30 1899. Dates prior to this are stored as negative values, those after are positive. In the example above, February 12 1998 is stored as 35838. You can test this outcome with the following code:

MsgBox CDbl(DateSerial(1998, 2, 12))

The Date data type is also able to hold time information. Hours, minutes, and seconds are held as fractions, with noon represented as 0.5. If we take the number of seconds in a day, 86400, and divide that into 1, the answer is the fraction equal to one second: 0.000011574…. The table below shows the minimum, default, and maximum values that can be stored in a variable declared as a Date.
 | Date | Value Stored
Minimum Value | January 1 100 00:00:00 | -657434
Default Value | December 30 1899 00:00:00 | 0
Maximum Value | December 31 9999 23:59:59 | 2958465.99998843

As we can see, there is nothing wrong with the way Visual Basic stores dates. Its method is both compact and Year 2000 compliant. For example, 8 bytes would store only the date if encoded as an alphanumeric CCYYMMDD. In effect, the Date data type allows us to store the time for free.

Manipulating Dates in Visual Basic

Once all your dates are stored in Date variables, all the date manipulation functions become available. The benefits of these functions are obvious—they are Year 2000 compliant and leap year aware.

Visual Basic has a number of date manipulation functions. In this section we are going to look at them in some detail. It might seem like I am telling you something you already know, but I have seen too many supposedly good Visual Basic developers remain unaware of the range of tools that are in the box.

Date tools

Visual Basic provides a lot of properties and functions that support comparison and manipulation of dates. These properties and functions are all designed to work with the Visual Basic Date data type and should be used in preference to all other methods. The majority of these elements reside in the VBA library in a class called DateTime. You can see the details of the class in Figure 8-1.

Figure 8-1 The VBA.DateTime class as seen in the Visual Basic Object Browser

TIP

With all the conversion functions, you would do well to use IsDate to test your expression before you perform the conversion.

The Calendar property This property exposes the calendar system currently in use within your application. By default this is set to vbCalGreg, the Gregorian calendar in use throughout most of the western world. Currently the only alternative is vbCalHijri, the Hijri calendar.

The Now, Date, Date$, Time, and Time$ properties All these properties perform similar tasks. They retrieve or assign the system date or time. By far the most used is the read-only Now property, which returns the current system date and time as a Visual Basic Date that can be assigned directly to a Date data type variable without conversion.

The Date and Time properties can be used to assign or return just the date or time part of the current system date. When assigning, the Date property expects to be passed a date expression containing the date you want to set the system date to. Any time information is ignored. The date must be within the range shown in the table below. Dates outside this range will result in a run-time error (5 - Invalid Procedure Call Or Argument). The Date$ property returns and assigns dates from Strings, with the equivalent Date property using Variants.

Range for VBA.DateTime.Date | Windows 9x | Windows NT
Minimum Date | January 1 1980 | January 1 1980
Maximum Date | December 31 2099 | December 31 2099

The Time and Time$ properties perform a task similar to Date and Date$, exposing the system time.

The Timer property This property returns the number of seconds that have elapsed since midnight.

The DateDiff function This function performs a comparison of two dates. The value that is returned—the difference between the two dates—is reported in a time or date unit of the caller's choosing. An important point to note is that the answer will correctly reflect the fact that the year 2000 is a leap year. The following code displays the difference, in number of days (specified by the first argument), between the current system date and December 1, 2000.

' Display the number of days until Dec 1 2000.
MsgBox DateDiff("d", Now, #12/1/2000# _
                , vbUseSystemDayOfWeek, vbUseSystem)

The fourth and fifth arguments are both optional, allowing you to specify the first day of the week and the first week of the year. Both will default to the system values if omitted.

The DateAdd function This function is used to modify a Visual Basic Date, with the value returned being the new Date following modification. Again this routine is fully aware of the leap year rules. The following line of code adds one month to the date January 31 2000 and returns the result February 29 2000, correctly calculating that February will have 29 days in the year 2000.

' Add one month to Jan 31 2000.
MsgBox DateAdd("m", 1, CDate("31 Jan 2000"))

The Year, Month, and Day functions The Format$ function is often abused when a programmer needs to get only part of the information held in a date. I still come across newly written code where Format$ has been used to do this.

' Getting the month of the current date, the old way
iMonth = CInt(Format$(Date, "MM"))

' And how to do it the new, more efficient way
iMonth = Month(Date)

Visual Basic provides the Year, Month, and Day functions to return these numeric values when passed a Date.

The Hour, Minute, and Second functions Not surprisingly, these functions perform a similar task to the Year, Month, and Day functions described above, except that they will return the numeric values representing the components of the time held in a Visual Basic Date.

The DatePart function This function returns the part of a passed date that you request in the unit of your choice. The above Year, Month, Day, Hour, Minute, and Second functions can perform the majority of the tasks that DatePart can, but the DatePart function does give you more flexibility, as demonstrated in the following code:

' Get the quarter of the current date.
MsgBox DatePart("q", Now, vbUseSystemDayOfWeek, vbUseSystem)

The third and fourth arguments are both optional, allowing you to specify the first day of the week and the first week of the year. Both will default to the system values if omitted.

The Weekday function This function will return the day of the week of the Date passed in as the first argument. The second optional argument allows you to specify the first day of the week.

' Get the current day of the week.
MsgBox Weekday(Now, vbUseSystemDayOfWeek)

The DateValue and TimeValue functions These two functions perform conversions from a String date expression to a Date data type; in this case the conversion will be of only the date for DateValue and the time for TimeValue. These functions are useful if you want to separate the two parts of a date for separate storage.

One point to note with these two functions is that you can get a Type Mismatch error if any part of the expression you are converting is not valid, even the part you are not interested in. Executing the code below will result in this error, even though the time part of the expression is valid.

' Try this; it causes a Type Mismatch error!
MsgBox TimeValue("29 02 1900 12:15")

The DateSerial and TimeSerial functions DateSerial and TimeSerial are less flexible than DateValue and TimeValue, requiring three numeric parameters to define the date or time you want to convert. The three parameters of the DateSerial function are the year, month, and day, in that order. TimeSerial expects hours, minutes, and seconds.

' Assign April 12 1998 to the date.
dteMyDate = DateSerial(1998, 4, 12)

' Alternatively, assign the time 12:00:00.
dteMyDate = TimeSerial(12, 00, 00)

Both these functions have an interesting ability to accept values outside the normal range for each time period (excluding years). For instance, if you pass the year 1998 and the month 14 to the DateSerial function, it will actually return a date in the second month of 1999, having added the 14 months to 1998. The following line of code illustrates this. (Your output might look different depending on your system settings, but the date will be the same.)

Debug.Print "The Date is " & Format$( _
             DateSerial (1998, 2, 29), "Long Date")
The Date is 01 March 1998

In this instance, DateSerial has correctly worked out that there is no February 29 in 1998, so it has rolled the month over to March for the extra day. We can use this ability to write a function that tells us whether any year is a leap year.

Public Function IsLeapYear(ByVal inYear As Integer) As Boolean

    IsLeapYear = (29 = Day(DateSerial(inYear, 2, 29)))

End Function

Formatting and displaying dates

 These functions can be found in the VBA.Strings module. All these functions are aware of the current system locale settings. Any strings returned will be in the language and style of this locale.

Locales have particular formats for such things as the date, time, and currency. For instance, a user on a PC in France would expect to read or be able to enter date information in a familiar format. Windows extends this formatting to cover common text such as the days of the week or the months of the year. Visual Basic is aware of the system locale and will use the information associated with it when interpreting and formatting dates.

The Format and Format$ functions The Format function and the Format$ function are interchangeable. These functions return a string containing the passed date in the specified format. By default there are seven predefined date formats, of which "Long Date" and "Short Date" are the most useful; these two formats coincide with the formats set in the Regional Settings dialog box, shown in Figure 8-2. You can access this dialog box from the Regional Settings option in the Control Panel. The user can use the Date property page of this dialog box to modify both the Short Date and Long Date formats. These formats are directly supported by the Format$ function.

Figure 8-2 The Windows Control Panel, Regional Settings Properties dialog box

If we convert a Date to a string without applying a format we will actually assign the date in General Date format. For the U.S. this defaults to M/d/yy; for the U.K. and much of Europe it defaults to dd/MM/yy. The code extract below will display the date in a message box using the system General Date format. (See the table on the following page for a description of the General Date format.) You can experiment by changing the Short Date and Long Date formats and rerunning the code.

Dim dteMyDate As Date

dteMyDate = DateSerial(1997, 2, 12)
MsgBox CStr(dteMyDate)

To use any named format other than General Date, we have to explicitly specify the format with the Format$ function. We can substitute the following line for the MsgBox line in the code above:

MsgBox Format$(dteMyDate, "Long Date" _
               , vbUseSystemDayOfWeek, vbUseSystem)

The third and fourth arguments are both optional, allowing you to specify the first day of the week and the first week of the year. Both will default to the system values if omitted.

The format types are very useful for displaying dates, either on line or within reports. Here the user has some control over the format via the Control Panel, and you maintain consistency with many other applications.

CAUTION

The size of date and time formats can be changed. As this is outside your application's direct control, you should allow sufficient space for any eventuality. Even when using the default General Date format we cannot assume a fixed length string. Dates in the 20th century will be formatted with two-digit years; dates in any other century, however, will be formatted with four-digit years. This behavior is consistent, even when we move the system date into the 21st century.

Notice that the formats in the table below are purely for coercing a Date into a String; they have no effect on the date value stored. A Date displayed using the Short Date format will still hold century information (indeed, it will hold the time too); it will just be coy about it. The Short Date format is particularly open to abuse, sometimes by so-called Year 2000 experts convinced that the PC problem can be solved by changing the Short Date format to include the century.

Format Name | Description
General Date

(Default)

This will use the system Short Date format.

If the date to be displayed contains time information, this will also be displayed in the Long Time format.

Dates outside 1930 to 2029 will be formatted with century information regardless of the settings for the Short Date format in the Regional Settings.

Long Date | This will use the Regional Settings system Long Date format.
Medium Date This will use a format applicable to the current system locale.

This cannot be set in the Regional Settings of the Control Panel.

Short Date | This will use the Regional Settings system Short Date format.
Long Time | This will use the Regional Settings system Time format.
Medium Time | This will format the time using a 12-hour format.
Short Time | This will format the time using a 24-hour format.

In addition to the predefined formats, you can apply your own formats. The weakness in using nonstandard formats for display purposes is that they are not controllable by the Regional Settings in the Control Panel. So if you are considering foreign markets for your software, you might have to modify your code for any change in regional date format (the different U.K. and U.S. formats are an obvious example). My advice is to use only the default formats wherever possible.

NOTE

Format$, DateAdd, and DateDiff are a little inconsistent with the tokens they use to represent different time periods. Format$ uses "n" as the token for minutes and "m" or "M" for months. However, DateAdd and DateDiff expect minutes as "m," and months as "M." Because the Regional Settings dialog box also uses "M," my advice would be to always use the upper-case letter when specifying the month in any of these functions.

If you convert a Date directly to a String without using Format, the resulting String will follow the general date rules except that dates outside the range 1930-1999 will be formatted with four-digit years, regardless of the settings for Short Date.

The FormatDateTime function This function is new to Visual Basic in version 6. It works in a similar way to Format$. However, FormatDateTime uses an enumerated argument for the format instead of parsing a string. This makes it less flexible than Format$, but faster. If you are going to be using only the system date formats, you should use FormatDateTime instead of Format$, giving you cleaner code and a slight performance improvement.

' Print the current system date.
dteMyDate = FormatDateTime(Now, vbLongDate)

The MonthName function Another addition to Visual Basic version 6, MonthName returns a string containing the name of the month that was passed in as an argument of type long. This function replaces one of the tricks that Format$ had often been called upon to do in the past: getting the name of a month.

' Give me the full name of the current month, the old way.
MsgBox Format$(Now, "MMMM")

' Now do it the new way.
MsgBox MonthName(Month(Now), False)

This function has a second, optional Boolean argument that when set to True will cause the function to return the abbreviated month name. The default for this argument is False.

The WeekdayName function WeekdayName is another addition to Visual Basic 6. It works in a similar way to MonthName except that it returns a string containing the name of the day of the week.

' Give me the name of the current day of the week,
' the old way.
MsgBox Format$(Now, "dddd", vbUseSystemDayOfWeek)

' Give me the full name of the current day of the week
' for the current system locale, the new way.
MsgBox WeekdayName(Weekday(Now, vbUseSystemDayOfWeek), _
                   False, vbUseSystemDayOfWeek)

Again, the remaining arguments are optional. The first, if set to True, will cause the function to return the abbreviation of the day of the week; the second tells the function what day to use as the first day of the week.

The conversion and information functions

The last set of functions we are going to look at are the conversion functions. The CDate and CVDate functions CDate and CVDate both convert a date expression (ambiguous or not) directly into a Date data type. The difference is that CVDate actually returns a Variant of type vbDate (7) and is retained for backward compatibility with earlier versions of the language. The following code demonstrates two different ways of using CDate to retrieve a Date.

Dim dteMyDate   As Date

' This assigns December 31 1999 to the date...
dteMyDate = CDate("31 Dec 1999")

' ...and so does this.
dteMyDate = CDate(36525)

CDate and CVDate perform a similar function to the DateValue function in the DateTime library with two exceptions. First, they can convert numeric values to a Date. The example above shows CDate converting the numeric serial date value of 36525 to a date of December 31 1999. Second, they will include time information in the conversion if it is present.

These functions can be found in the VBA.Conversion module, along with the other conversion functions such as CLng and CInt.

The IsDate function This function performs a simple but vital task. If passed a date expression, it will return True if the expression can be converted to a Visual Basic Date successfully. This is of great use when validating dates from sources directly outside your control, such as the user (the bane of all developers' lives).

If True = IsDate(txtDateOfBirth.Text) Then
    ' Convert the expression entered to a date.
    dteDOB = CDate(txtDateOfBirth.Text)
Else
    ' Otherwise, inform the user of his or her mistake.
    MsgBox "Don't be silly. That is not a valid date."
End If

To add a final bit of complexity to everything, this function lives in a fourth module, VBA.Information.


Appendix A

Coding Conventions

This section is based largely upon our original coding conventions appendix in our first edition, written by Mark Hurst and Peet Morris. It describes the coding conventions we used in preparing most of the source code for this book. These conventions are based on extracts from our in-house TMS Visual Basic programmer's manual, VB Vogue (also included as part of the TMS Developer's Framework). You can use this appendix purely to look up type or scope prefixes you don't understand, but you might also want to use our conventions in your own code. Or you might decide to adapt them to suit your own preferences. The important thing is that you have coding conventions and that you apply them uniformly across your code. (If your company is interested in purchasing VB Vogue, please send e-mail to the info@TheMandelbrotSet.com alias requesting details and availability.)


The Need For Coding Conventions

Consistently applied coding standards can improve the following in a software project:

  • Productivity. Productivity, as applied here to the actual writing of code, means saving "code time" without suffering some inherent quality penalty.

  • Quality. The quality of code is chiefly measured by its relative conformance to some technical specification and the ease with which it can be maintained.

  • Maintainability. Maintainable code is both repairable and extensible. Additionally, and in all cases, maintainable code is easy to comprehend, which facilitates achieving speedy, accurate modification.

  • Comprehensibility. Comprehensible code is both conformant (with some recognized standard) and consistent.


Naming Conventions

We use a simplified form of Hungarian notation, a naming convention that takes its name from the nationality of its creator, Charles Simonyi. Certain elements of Hungarian notation are used in Microsoft's Visual Basic manuals. Microsoft also uses Hungarian notation internally as part of its coding conventions, as do many developers around the world. Our simplified form of this notation is used to attach both type and scope information to various object names. Hungarian notation uses character prefixes to denote type, aggregation, and scope, and the full system gets rather complicated. The subset we've chosen for our purposes is defined in the following sections and in the tables that appear at the end of this appendix.

Type Information

Hungarian type prefixes have two parts: a base type to represent the base data type and a modifier to denote an aggregate type. An example of a base-type prefix is n, which denotes an integer, while adding the a modifier to the base type denotes an array of integers. An example should make this clearer:

Dim nCounter            As Integer
Dim anCounters(1 To 10) As Integer

Notice that the variable name itself starts with an uppercase letter, which shows us where the prefix stops and the variable name starts. Sometimes we need to use more than one modifier, such as a multidimensional array:

Dim aanStateTable(1 To 10, 1 To 10) As Integer

The base types and modifiers we use are listed at the end of this appendix. In fact, we use only one aggregate modifier since arrays are the only single-type aggregates that Visual Basic supports.

Where calls are made to Windows API functions, you might see type prefixes such as LPSTR and sz that don't appear in our tables: these prefixes are used in the Microsoft Windows documentation. We've found that changing these to match our Visual Basic data types can often conceal errors in the declarations. You might also see p used as a modifier to denote a pointer and pcb to denote the address of a callback function.

For variables defined with user-defined types (UDTs) and classes, the special base-type prefixes t and C are used. Although C breaks the lowercase rule, we've adopted it because it's used throughout the industry.

Menus are named with the type prefix mnu, but the names are aggregated to show the menu structure. For example, the Open item on the File menu is called mnuFileOpen. Here are some more standard menu names:

mnuHelpAbout
mnuFileExit

Scope Information

We also use prefixes to denote an object's scope. Our scope prefixes come between the type prefix and the object name, so we need to make sure they start with an uppercase letter and end with a lowercase letter. This way, the scope prefixes stand out from both the type prefix and the object's name. For example, a private integer variable (scope prefix Pi) defined at the module level would be named like this:

Private nPiCounter As Integer

Variables

Variables are named like this:

<type><scope><name>

The name part is simply the variable name written in mixed case, and the type part is a Hungarian type as defined earlier. We don't use type and scope prefixes when naming properties and methods of classes and forms.

The base types and modifiers are given in a table at the end of this appendix. The scope part is defined in the following sections.

Local variables

Local variables do not have a scope prefix. Here are some examples of local variable definitions:

Dim nCounter  As Integer
Dim sMessage  As String
Dim tThisCell As tTableEntry  ' A user-defined type
ReDim anLookupTable (1 To 10) As Integer

Private variables

Private variables defined at the module level have Pi as a scope prefix. Some examples follow:

Private nPiCounter  As Integer
Private sPiMessage  As String
Private tPiThisCell As tTableEntry  ' A user-defined type
Private anPiLookupTable () As Integer

Global variables

Public variables defined at the module level of a standard module (that is, a BAS file) have the module identifier as a scope prefix. The module identifier is a unique two-character prefix that suggests the module name. For example, we might choose Er for an error handling module or Db for a database module. We also use an additional scope prefix, pu, to identify the variable as public. Here are some examples.

Public nPuErCounter As Integer          ' Er for "error handling"
Public sPuErMessage As String
Public anPuTbLookupTable () As Integer  ' Tb for "table functions"
Public tPuTbThisCell As tTableEntry     ' A user-defined type

Form and class properties (unprotected)

Public variables defined at the module level of classes and forms are properties and do not have scope or type prefixes.

Functions and Subroutines

Public functions and subroutines have scope prefixes in the same way that variables do. Public and private functions also have a type prefix that reflects the type of value returned. The rules for choosing the type prefix are the same as those for variables.

Private subroutines

Private subroutines and functions do not require scope prefixes. Here are some examples of private subroutines:

Private Sub OpenLogFile()
Private Sub ClearGrid()

And here are some private functions:

Private Function nGetNextItem() As Integer
Private Function sGetFullPath(ByVal sFileName As String) _
As String
Private Function CGetNextCell() As CTableEntry

Public subroutines

The rules for choosing scope prefixes for public subroutines are exactly the same as those for variables, as shown here:

Public ErReportError()
Public TbInsertTableEntry()
Public DbDeleteItem()

Form and class methods

Public functions and subroutines defined in classes and forms are properties and do not have type or scope prefixes.

Form and class properties (protected)

Property Let, Property Set, and Property Get routines do not have type or scope prefixes.

DLL procedures

When declaring Windows API functions, we use the Alias keyword to add the prefix Win. For example, CallWindowProc becomes WinCallWindowProc. When declaring functions in other DLLs, we use the prefix Dll.

Parameter lists

Formal parameters are named as local variables but with i or o added in place of a scope prefix to denote whether the parameter is an input or an output:

Sub CrackURL(ByVal siURL As String, ByRef soProtcol, _
    ByRef soPath, ByRef soPath, ByRef noPort)

For parameters that are both inputs and outputs, we use io.

Controls and Forms

We also use type prefixes when naming Visual Basic controls. The prefixes are given at the end of this appendix. Forms are named as if they were controls, even though a form name can also act as a class name. For example, we would name a form frmPublication instead of CPublication, even though we could create new instances of the form:

Dim frmNewPub As New frmPublication

Constants

Constants are named in uppercase letters, with no type or scope prefixes. Arbitrary prefixes can be used to group sets of related constants:

Const TABLE_SIZE            As Integer = 100
Const DEFAULT_LOG_FILE      As String = "log.txt"

Const ERR_GENERIC           As Integer = 32767
Const ERR_LOGON_FAIL        As Integer = 32766
Const ERR_COMPONENT_MISSING As Integer = 32765

Type and Class Definitions

Type names are prefixed with t, and class names are prefixed with C. These prefixes are also used as the base-type prefixes when naming instances. The rest of the name is written in mixed case:

Type tTableEntry
    nNewState As Integer
    lAction   As Long
End Type

And in use:

Dim tCell         As tTableEntry
Dim CSymbolTable  As New CTable

Source Files

Source filenames are restricted to the traditional 8.3 format. The first three characters are reserved for a project identifier, and the default file extensions are retained. The remaining five characters are used to distinguish files in whatever way seems appropriate.

OtherNaming Conventions

This is an example of a local string:

Dim sCustomerName As String

This is an example of a module-level integer:

Dim nmCustomerId As Integer

Here is a global-level Double defined in the module Mi:

Public dPuMiCustomerNumber As Double

Here is a global-level Double; the module identifier is not defined or else resident in the project-wide globals module.

Public dPuCustomerNumber As Double

This is a global array of Longs. The module identifier is not defined or else resident in the project-wide globals module.

Public alPuAmounts(1 to 25) As Long

This is a global database. The module identifier is Dt:

Public dbPuDtCustomers As Database

This shows a call to an external DLL function; the result is assigned to a global variable resident in the project-wide global's module.

bPuRetCode = bDllIsShareLoaded ()


Coding Conventions

This section outlines a number of conventions we have found useful when writing code. It isn't comprehensive—our in-house manual, VB Vogue, contains much more detail than we have space for here. We suggest you use this section as a basis for designing your own coding standards.

Principles

Always code for clarity, not efficiency. Choose variable and function names carefully. Be verbose rather than terse, and parenthesize expressions to make them clearer. Write your code for the reader.

Limit the scope of constants, variables, functions, and subroutines as much as possible. If you have the choice, always use a local object in preference to a global one.

Use explicit type conversions (CInt, CStr, and so on) when assigning between different variable types.

Some Specifics

Follow these guidelines when you write code:

  • Define all variables explicitly. Checking the Require Variable Declaration option under Tools/Options/Editor enforces this. Define variable-length arrays explicitly. (Visual Basic will not enforce this.)

  • Always specify the upper and lower bounds in an array definition.

  • Define constants, and use them in place of numbers in the code.

  • Always use the ByVal and ByRef keywords in parameter lists. Use ByVal where you have a choice.

  • Always use the Private keyword on functions and subroutines that are not exported from a module.

  • Define the types of all variables and function and subroutine parameters explicitly. For example, use Dim nBufSize As Integer instead of Dim nBufSize, and use Function nGetChars(nBufSize As Integer) As Integer instead of Function nGetChars(nBufSize).

  • Do not use implicit type suffixes (such as ! and &) in variable definitions.

  • In For loops, use Next <Index> instead of just Next.

Follow the general principle of restricting the scope of variables and functions to the barest minimum possible. However, be sure to use common sense. For example, a procedure that logically serves only one form should be "contained" within that form. Likewise, a procedure that could logically serve many forms should be placed in a module, even though it might currently be called only from one form.

Miscellaneous Conventions

This section describes some other conventions worth noting.

Dimensioning variables

Variables should each be dimensioned individually, meaning there should be only one Dim, Public, Private, or Static per variable. Multiple dimensions on a line can lead to ambiguity about the scope and type of a variable.

Defining arrays

Both the upper and lower bounds in array definitions must be specified. Do not rely on the Option Base setting (Option Base should not be used).

Public anDigits(10) As Integer

This dimensions 11 entries in the array if the Option Base is equal to 0, which is its default.

Public anDigits(1 to 10) As Integer

This dimensions 10 entries in the array, regardless of the Option Base setting, and is the way to dimension arrays.

Using control arrays

Control arrays must be used whenever possible and whenever appropriate in Visual Basic. These make smaller executables, which therefore lead to better performance.

Using operators

Use only the addition operator (+) in numerical expressions. Do not use it to facilitate some string concatenation. This is especially true if using variables of type Variant.

Comparing strings

StrComp is by far the fastest way to compare strings. Use it instead of a language operator (such as =) whenever possible. Binary Comparison is faster than Text Comparison (especially if you're using the Like operator), and should be used whenever it is appropriate. Generally, using fixed strings is slower, with the exception of passing strings to API or DLL procedures.

Using parentheses

While a parenthesized expression might be slower to evaluate than a non-parenthesized expression, parentheses should be applied where appropriate to ensure the readability of code.

Initializing variables

Initialize variables through assignment. Initialization or pre-assignment (in the case of Visual Basic) of variables is a good programming habit to get into.

Initialize variables as close as possible to where they are used. This avoids having to hunt for the initialization:

Dim nCount As Integer : nCount = 1 ' for counting iterations

or

Dim nCount As Integer ' for counting iterations
nCount = 1             ' initialized

Calling subroutines

The Call keyword should prefix any call to a subroutine, since it makes explicit what is happening in the code. Using Call also avoids the problem of an apparent parameter being merely evaluated.

Setting control properties at design time vs. run time

As a general rule, it is a good idea to do as much in the code as possible. For example, the Mask property of a masked-edit control is best set "in code" at run time rather than at design time, because setting it in code generally makes clearer what is going on. This method also makes code more portable (because operational logic is not tied to any form property, such as in the previous example, the code is more portable and generic).

Using For...Next statements

In For loops, use Next <counter> instead of just Next. In For Each loops (VBA), use Next <element> instead of just Next. These recommendations are faster.

For...Next loops are more than twice as fast as any equivalent Do...Loop and While....Wend loops. Not surprisingly, Integer counters are the fastest of all the variable types to use as a counter.

Using the Me statement

The Me statement should be used whenever possible in the code. Where code in an object module (class or form) refers to objects in the same module, use Me rather than the object's name to make the reference implicit, and be assured the code will work with multiple instances of the object.

NOTE
It is not possible to use Me when setting an instance of a class or form object to Nothing.

Optional parameters

Visual Basic's typed optional parameters are faster than untyped (Variant) optional parameters. However, note that IsMissing does not work with typed optional parameters, only with optional Variant parameters.


In-Line Documentation

One of the best sources of documentation is the code itself. When you follow a specific coding format and thoroughly comment your code, you're making your program more readable and providing valuable information to yourself and to other programmers who might have to make modifications to the code later.

Formatting

Follow these rules when formatting your code:

  • Punctuate your code with blank lines.

  • Keep your procedures to a manageable size—preferably not much more than a single screen.

  • Keep indentation under control. If you're having difficulty remaining within the right margin, you're probably nesting too deeply.

  • Use parentheses to enhance the readability of your code. No one reading your code should have to wonder what you meant when you wrote it.

  • Preserve screen real estate wherever possible. For example, when creating a long string, it might be better to build up the string with multiple assignments:

    Dim sMsg As String

sMsg = "This is a paragraph that is to appear "
sMsg = sMsg & "in a message box. The text is "
sMsg = sMsg & "broken into several lines of code "
sMsg = sMsg & "in the source code, making it easier "
sMsg = sMsg & "for the programmer to read and debug. "

MsgBox sMsg, vbOkOnly, "Text"

Dim sQRY As String

sQRY = "SELECT Customer.CustomerName, Orders.OrderValue "
sQRY = sQRY & "FROM Customer, Orders "
sQRY = sQRY & "WHERE Customer.CustomerId = Orders.CustomerId"

ReportQry1.SQL = sQRY

Code Commenting

The comments in and around the code and, to a lesser though highly important extent, the names and identifiers used within the code, constitute the readable English in a program. Both individually and as a collective comments animate the code to the reader and help explain what is happening.

Comments typically center upon some sequence point, around which some current "code-wise" activity focuses. In other words, comments explain what has happened or is about to happen in the code. Comments must always therefore provide a narrative or biography of the code—past and present. Additionally, comments are not contained within the "built" Visual Basic code and as such have no overhead attached to them. Don't be afraid to comment code in the belief that it will cause code bloat and the like.

The important rules for commenting are as follows:

  • Keep comments up to date and directed towards being excessively verbose, if anything.

  • A good comment is a high quality comment. Quality, not quantity, is all important.

  • Use capital letters to issue warnings and signal important announcements.

  • Comments must be clear and concise—always attempt to add value and never simply restate the obvious.

  • Use short comments to say what is happening and longer comments to say how and why the state transition is occurring.

  • To reduce the number of comments you use, try to write code that does not require commenting.

  • By using clear sentences and parallel structure in comments, you avoid ambiguities and any resulting confusion as to your intent.

End-of-line comments

End-of-line comments should be used to annotate variable or constant definitions:

Const MAX_ROWS As Long = 2147483647 ' (2^31)-1 (Max value for
                                    ' long integer)

Const ICONIZED As Integer = 7   ' To startup applications
                                ' with Shell().

In-line comments

End-of-line comments are rarely suitable for annotating code. Where a function or subroutine contains a number of logical blocks, introduce each block with a separate comment indented to the same level as the code. Use as many lines as necessary, and don't use surrounding ***** lines to delimit comments unless something really important is about to happen. Keep comment lines short so that the entire comment can be viewed on a VGA display without scrolling left and right.

Use comments to explain the code, not to echo it; if your comments become very involved, consider restructuring the code. Complicated explanations can also be moved into the function header. The following code shows some examples of appropriate in-line comments:

MaxCol = nColumn - 1
XL_NewRow stDataBlock
XL_NewRow stDummyRow

' The dummy row now holds the long form of the column headings.
' Appending this to the main data block gives us both long and
' short headings, one below the other.

XL_AppendBlock stDataBlock, stDummyRow

' We now need to step through the data array and extract the
' records for each row in turn. Records corresponding to a
' single row all have the same code and description and are
' contiguous in the array (which is sorted on description).
' The row description array returned by GetNextRow is sorted
' on column number.

nDataIdx = 0
Do While nDataIdx < nNumRecs

File Headers

Every module should have a header that sits as a comment in the module's definitions area. The header identifies the module and summarizes the external interfaces to it. Here is an example module header:

' **************************************************************
' Module            Startup
'
' Filename          tabstart.bas
' Module Prefix     St
'
' Author            A. Developer
'                   The Mandelbrot Set (Int'l) Ltd.
'
' Description
'
' Startup module for the table-driven FSM sample application
'
' Revisions
' 11-12-97, A.Developer
' Added instance checking.
'
' 08-12-97, A.Developer
' Moved global FSM object out of here.
' **************************************************************

The module prefix is a two-letter code used as a scope prefix that uniquely identifies this module in the project. Module prefixes are significant only for standard (BAS) modules.

Function and Subroutine Headers

A function header is a comment that describes what the function does and summarizes its interface. The description should focus on what the function does, although for complicated or longer functions it might be appropriate to summarize the how as well. All nontrivial functions should have function headers, and headers are also recommended for nontrivial event handlers.

Here is an example function header:

' **************************************************************
'
' Synopsis      Create the event queue, and attach
'               an event handler to it.
'
' Parameters
'
'   pcbiNewWinProc          (I) Address of the event
'                               queue callback function
'
' Nonlocal Data
'
'   hwndPiEventQueue        (O) Event queue handle
'   pcblPiEvQueueOldWinProc (O) Event queue default
'                               window procedure
'   lPiFSMEventMsg          (O) Event message number
'
' Description
'
' This is where we create the event queue and attach a
' callback function to it to handle our FSM events.
'
' The event queue is built around a private window that
' we create here. We subclass the window to hook
' pcbiNewWinProc onto it and then register a custom
' message number that we will use to send messages to it.
' The pcbiNewWinProc parameter is a pointer to a VB
' function obtained with the AddressOf operator.
'
' **************************************************************


Type Prefixes

In this section, you'll find tables that include prefixes for various data types and control types.

Table A-1 Variables Prefixes
Prefix | Data Type
byt | Byte
b | Boolean
cur | Currency
d | Double
dte | Date
f | Single
hf | File handle (Long)
hwnd | Window handle (Long)
h(... lowercase) | Handle to something (Long)
l | Long
n | Integer
o | Object
s | String
v | Variant

Table A-2 Modifiers and Special Types Prefixes
Prefix | Data Type
a | Array <of another type>
C | Class or class instance
t | User-defined type or instance
e | Enumerated type or instance
p | Pointer (used with API calls)
pcb | Pointer to a callback function (used with AddressOf)

Table A-3 Data Objects: DAO Prefixes
Prefix | Visual Basic Data Type
bk | SelBookmarks
ct | Container
db | Database
dc | Document
ds | Dynaset
er | Errors
fd | Field
gp | Group
ix | Index
pa | Parameter
pr | Property
qd | QueryDef
rs | Recordset
rl | Relation
ss | Snapshot
tb | Table
td | TableDef
us | User
wk | Workspace

Table A-4 Data Objects: RDO Prefixes
Prefix | Visual Basic Data Type
rdoEng | rdoEngine
rdoEnv | rdoEnvironment
rdoConn | rdoConnection
rdoTbl | rdoTable
rdoCol | rdoColumn
rdoPrepS | rdoPreparedStatement
rdoParam | rdoParameter
rdoRS | rdoResultset
rdoErr | rdoError

Table A-5 Controls Prefixes
Prefix | Control Type Description
ani | Animation button
bed | Pen BEdit
cbo | Combobox/Dropdown Listbox
chk | Checkbox
clp | Picture Clip
cmd | Command Button
com | Communications
dat | Data Control
dir | Directory Listbox
dlg | Common dialog
drv | Drive Listbox
fil | File Listbox
fra | Frame
frm | Form
gau | Gauge
gpb | Group Pushbutton
gra | Graph
grd | Grid
hed | Pen HEdit
hsb | Horizontal scroll bar
img | Image
ink | Pen Ink
key | Keyboard key status
lab | Label
lin | Line
lst | Listbox
mci | MCI
mnu | Menu
mpm | MAPI Message
mps | MAPI Session
ole | OLE Client
opt | Option Button
Prefix | Control Type Description
out | Outline Control
pic | Picture
pnl | 3D Panel
rdc | Remote Data Control
shp | Shape
spn | Spin Control
tab | SS Tab Control
tmr | Timer
txt | Textbox
vsb | Vertical scroll bar
iml | ImageList
lvw | ListView
pbr | ProgressBar
rtf | RichTextBox
sld | Slider
sbr | StatusBar
tab | Tabstrip
tbr | Toolbar
tvw | TreeView

Table A-6 Data-Bound Controls Prefixes
Prefix | Control Type Description
dbcbo | Databound Combobox/Dropdown Listbox
dblst | Databound Listbox
dbgrd | Databound Grid


Other Conventions

Other conventions not listed above can be found in other chapters of this book. For instance Chapter 1 explains the error handling strategy we use at TMS. Chapter 8 provides a full discussion on how we mandate that dates should be handled in Visual Basic.


Appendix B

Sample Answers to the Technical Test in Chapter 17

 Refer to Chapter 17 for the questions to this exam given by TMS to developer candidates.

A1.

-1 -1 -1 .

The = operator in Visual Basic is used for both assignment (such as "a is assigned…") and comparison (such as testing the assertion that b has the same value as c—note the disuse of "equivalent"). The value &HFFFF is synonymous with the value of True and -1, so testing to see if -1 is the same value as &HFFFF yields True, or -1. As an aside, try changing Integer to Long throughout the code; you might be surprised to see -1 -1 -1 output here, too. For yet another result, try changing &HFFFF to 65535 (and use Longs)! Finally, try using 65535 with Integer. You'll see it causes an overflow error. A modification to the question, then, is to pose it again using one of these alternative forms and to ask why the results differ.(The answers are exercises for the reader.)

A2.

Code fragment A: Print the contents of the fourth item in the list box lstResults in the Debug window.

Code fragment B: Iterate through every item within the list box lstResults.

Code fragment C: If a selection has not been made in the list box lstResults, then …

Code fragment D: For each item up to the selected item, copy the index of each item within the list box lstResults into its ItemData property.

A3.

Public Function sNumToBin(ByVal nDecimal As Integer) As String

    Dim sBin  As String
    Dim nloop As Integer

    For nloop = 0 To 15

        sBin = IIf(nDecimal And 2 ^ nloop, "1", "0") & sBin

    Next

    sNumToBin = sBin

End Function

A4.

Public Function itoh(ByVal nByte As Byte) As String

    Const c As String = "0123456789ABCDEF"

    itoh = IIf(nByte <= 15, Mid$(c, nByte + 1, 1), "?")

    ' OR perhaps...

    itoh = IIf(nByte <= 15, Hex$(nByte), "?")

    ' OR perhaps...

    If nByte <= 15 Then
        itoh = Mid$(c, nByte + 1, 1)
    Else
        itoh = "?"
    End If

End Function

Many answers are possible—some might even include the use of the Choose and Switch functions.

A5.

Option 1: Insert the following code into the Form_Load procedure.

Me.Move (Screen.Width - Me.Width) \ 2, _
        (Screen.Height - Me.Height) \ 2

Option 2: Set the form's StartUpPosition property to CenterScreen at design time.

Screen resolution should not matter. Notice the use of the Integer division operator above; this might show you that someone is conscious of the language (knows about this operator) and is aware that clock cycles are more efficient.

A6.
Identifier | Type | Scope | Answer
BM010 | Form | Form | frm
FileOpen | Menu | - | mnu
RecordCount | Integer | Global | gn
FirstName | String Array | Modular | mas
VehclNum | Long | Local | l
Vehicles | Combo Box | - | cbo
Time | Label | - | lbl

Any "sane" prefix is fine, of course.

A7.

  1. The code fragment has a Function SomeFunc and a Sub SomeSub, both the Sub and the Function have ByRef arguments, and neither return a value. The code cmdTest_Click calls both routines. When SomeSub is called, a copy of the argument is passed overriding the effect of the implicit ByRef in the Sub declaration. When SomeFunc is called the argument is passed ByRef as declared. The output to the debug window will be 1 and 2.

  2. Both arguments get passed ByVal (to a copy of the actual argument), no errors occur, and the output to the debug window will be 1 and 1.

  3. The actual argument now gets passed ByRef; the output to the debug window will be 2 and 2.

  4. Yes, as in Part 2, the output to the debug window will be 1 and 1 because only a copy is passed.

  5. Pass by reference is faster.

As an aside, if SomeSub passes ByRef (as shown in the question), SomeSub (x) succeeds, forcing the "evaluation of x" to be passed rather than x itself—in other words, forces a ByVal result. (The "evaluation of x," called an anonymous object, is a value [the result] stored in a memory location other than that occupied by x.) However, calling SomeSub as SomeSub ByVal x doesn't work. It might be worth posing this variant of the question and asking what the difference is. (The answer is an exercise for the reader.)

A8.

Private Sub Form_Load()

    Dim nLoop  As Integer

    Const COLS As Integer = 3
    Const ROWS As Integer = 4

    Me.Caption = "Calculator"
    labDisplay.Caption = ""

    With cmdDigit(nLoop)

        ' Size form, label, and initial cmdDigit to look right.
        Me.Height = ROWS * .Height + labDisplay.Height + Me.Height - Me.ScaleHeight
        Me.Width = COLS * .Width + Me.Width - Me.ScaleWidth

        labDisplay.Move 0, 0, Me.Width - (Me.Width - Me.ScaleWidth), labDisplay.Height

        .Move 0, Me.ScaleHeight - .Height

    End With

    For nLoop = 1 To ROWS * COLS - 3

        Load cmdDigit(nLoop)

        With cmdDigit(nLoop)

            .Move cmdDigit(nLoop - 1).Left + .Width, _
                  cmdDigit(nLoop - 1).Top

            If 0 = nLoop Mod COLS Then
                .Top = .Top - .Height
                .Left = cmdDigit(0).Left
            End If

            ' Only want 10 numeric buttons so make 'extra'
            ' buttons non-functional.
            .Caption = IIf(nLoop <= 9, CStr(nLoop), "")

            .Visible = True

        End With

    Next

End Sub
Private Sub cmdDigit_Click(Index As Integer)

    labDisplay.Caption = labDisplay.Caption & cmdDigit(Index).Caption

End Sub

A9.

This list isn't complete. Also, some assume that we're using the Option Compare Text statement.

InStr(1, "TMS", sSource, 1)

InStr(Ucase$("TMS"), Ucase$(sSource))

sSource = "TMS"

StrComp(sSource, "TMS")

A10.

Function NTrim$(ByVal sString As String)

    Dim nPos As Integer

    nPos = InStr(1, sString, vbNullChar)

    Do While nPos <> 0
        sString = Left$(sString, nPos - 1) & _
                  Mid$(sString, nPos + 1)
        nPos = InStr(1, sString, vbNullChar)
    Loop

    NTrim$ = sString

End Function

Or:

Function NTrim$(ByVal sString As String)

    Dim nPos As Integer

    nPos = InStrRev(sString, vbNullChar)

    Do While nPos <> 0
        sString = Left$(sString, nPos - 1) & _
                  Mid$(sString, nPos + 1)
        nPos = InStrRev(sString, vbNullChar)
    Loop

    NTrim$ = sString

End Function

A11.

Function bFileExists(ByVal sFile As String) As Boolean

    On Error Resume Next

    Call GetAttr(sFile)

    bFileExists = True = (0 = Err.Number)

End Function

This is another question for which there are many possible answers. Whichever answer is given, ensure that hidden files, system files, and folder files (directories) are detected—you're looking for the "been there, done that" answer. For example, the normal bFileExists = "" <> Dir$(sFile) (used as shown) won't work for such files; a developer who doesn't realize this might cause crucial code to fail, such as the following:

Sub MakeFolder(ByVal s As String)

    If Not bFileExists(s) Then MkDir (s)

End Sub

A12.

Property procedures actually protect the property by only allowing (or disallowing) access to it through these procedures (Get/Let and Set); they are therefore safer. Property procedures can be used to filter invalid property assignments and give the programmer the option to validate assignment values before actually applying them. A Public variable (an item of data declared Public in a class) can be arbitrarily accessed and changed. (Many programmers, usually the less experienced ones, don't actually know how to use Property Set. This question might enlighten you as to who does and who doesn't.)

A class can only make data or functional properties (either explicitly or through property procedures) and methods visible outside of itself.

A13.

Code fragment A: Form Initialize only. No form instance is created. The application terminates immediately.

Code fragment B: Form Initialize only. No form instance is created. The application terminates immediately.

Code fragment C: Form Initialize and Load. There will be one (invisible) instance of Form1 once this code has executed. The application doesn't terminate.

A14.

The code is legal. The output to the Debug Window is as follows:

30
10
30
20

A15.

Optional Arguments: Look out for the use of Variant as parameter type and the use of the IsMissing function to test for presence of optional parameter.

Call MsgBox( _
             sRequired _
           , vbOKOnly _
           , IIf( _
                  IsMissing(sDiscretionary) _
                , App.EXEName _
                , sDiscretionary _
                ) _
           )

Parameter Arrays: Look for the final parameter to be declared as ParamArray.

Private Sub ParamTest(ByVal sMsg As String, ParamArray nArray())

    Dim nLoop As Integer

    For nLoop = Lbound(nArray) To Ubound(nArray)

        Debug.Print sMsg & CStr(nArray(nLoop))

    Next nLoop

End Sub

Collections: Look for the creation of a collection object as a "New Collection," then look for classes to be added to the collection object using the Add method. Then look for the For Each operation to iterate through the members of the collection. (An answer in which dictionaries are mentioned demonstrates knowledge in Visual Basic 6.)

Dim cColl As New Collection

cColl.Add item := Class1, key := 12

For Each Class1 In cColl
    .
    .
    .
Next cColl

A16.

VBCP was the Visual Basic Code Profiler add-in. It was used to ascertain the execution time of code and to find performance bottlenecks.

A17.

This code outputs Class1 onto the form.

A18.

n = ((((True And (&HFF + True)) And _
    ((&HFF00 + (Not Not True)) = Not False)) Or _
    (&HFF + (Not True))) Or &HFF00)

-1

You're looking for any mention that an expression like (assuming n is 32) n = Not n = 32 (where n is now 0, or False if used in a Boolean expression) is very different from n = Not n (where n is now -33, or True if used in a Boolean expression).

A19.

Wherever code needs to branch, unconditionally, to a new location within a procedure. An answer along the lines of "I'd never use it because it's bad practice" (does this imply the candidate doesn't use error handlers?) probably ought to be discussed. For example, ask the candidate to explain why he or she wouldn't use it—perhaps you can also have the candidate compare GoTo with other unconditional jumps, such as Procedure Call, Exit, Exit Loop, Exit For, Exit Do, Exit Sub, and so on. Make a point of asking the candidate to explain the difference between If SomeCondition Then GoTo and If SomeCondition Exit Loop.


Appendix C

TMS Developer's Framework

Since 1995 all TMS applications have been built using three interlocking, key building blocks. Collectively these building blocks are known as the "TMS Developer's Framework" (TDF):

  • VB Vogue

  • Application Template

  • Vogue Tools

Each building block can be purchased separately or as a set of complementary components that constitute the "Framework." Each element of the TDF is briefly explained below.

VB Vogue

VB Vogue is TMS's Visual Basic "Developer's Discipline" and Coding Standards reference document. It currently numbers around 70 pages.

VB Vogue is much more than a set of traditional coding standards. As well as defining naming conventions and coding style guidelines, it also includes sections on Year 2000 "readiness," component versioning, guidance on data access methods and structured exception handling techniques—to name but a few. Throughout, it is VB Vogue's aim to give clear explanations and justifications for any advice or rules that it mandates. During VB Vogue's development TMS made a conscious decision that it be much more than an informational reference book. Our key objective was to ensure it was also instructional and educational. The result: a valuable asset for any development team.

Application Template

TMS's Application Template, or App Template, contains around 18,500 lines of tried and tested infrastructure and support code, and is used on some of the world's biggest Visual Basic development projects. The App Template is actually made up of forms, classes, and modules that collaborate in assisting developers to support the advice or rules given in VB Vogue, allowing the developers to be productive and professional in how they code. For example, the App Template contains the routines necessary to log exceptions using a variety of methods (file, NT event log, database, and so on). Structured exception handling, as we know from development "best practice" (endorsed by VB Vogue), is a mandated facet of any professional development project. By allowing the developer to easily support such professional features, the overhead and burden on the developer is removed from any individual. The result is that any overhead incurred in adding (in this case) structured exception handling is effectively removed. Through logging the precise nature and location of errors, the Debug, Build, and Test cycle is far more efficient and controlled, and in turn offers substantial cost savings. In addition, once the product is shipped, the subsequent pressure on the support/help desk is significantly reduced.

Here is a small taste of what the App Template contains: platform and version checking; standard forms; enhanced replacement Visual Basic intrinsic objects such as Err, App, and Debug; installation support; "real" assertions; storage support (for accessing the Registry, etc.); string handling; and of course, structured exception and error logging.

Ever since its inception the App Template has been revised for each new release of Visual Basic and/or whenever a TMS developer can justify a reasonable modification. For example, inbetween Visual Basic 4 and Visual Basic 5, one revision was increased support to assist developers with the Year 2000 problem. This included providing standalone routines to check the validity of any entered date, and a new control. The template contains ancillary—although very valuable—components such as the DateBox control. This is an OCX (built using Visual Basic 5 and 6) designed to replace any TextBox control that is used to hold and/or manipulate dates. A DateBox, unlike a TextBox, mandates that any input sent to it should be "Y2K Safe" and at the same time "map" to whatever the user has set as a preferred Short Date setting. The DateBox control is sophisticated and includes many advanced features such as the ability to define any day of the week as invalid (so that the user cannot, for example, enter a date that is in actuality a Sunday where the date of a "working day" is expected). The control can be set, through design time properties, to generate a variety of behaviors that describe how it should respond to invalid dates. For example, should it generate a Visual Basic exception; should it prompt the user with a customizable error message; should it force the user to reattempt the data entry. And if the answer to the latter is yes, how many times should it force the user to retry before generating an exception.

At the time this book was being written App Template was being re-architectured into a set of controls, wizards, add-ins, modules, classes, and server-side objects.

In summary, the App Template contains everything that most serious applications should have but isn't part of their specialist function.

Vogue Tools

From the descriptions of VB Vogue and App Template, the user will see that the application of either involves taking actions that will directly manipulate code or form definitions. For example, to fully use the DateBox control a developer must replace all TextBox controls used to capture dates with DateBox controls. Obviously, this will result in a drawn out, laborious, and mostly manual alteration of the project's code. Also, as we saw, VB Vogue mandates that exception handlers be added to every routine in a project—again, a tedious manual process would result. Such necessary tedium typically dictates that these critical alterations and additions to the code base are simply not carried out, or that they are applied piecemeal throughout the project. Obviously, both situations are unacceptable and need rectifying.

Vogue Tools are designed to ease any pain involved in applying such rigorous development practices as outlined in VB Vogue and in the updating of existing code, to use, say, the DateBox control. For example, to assist a developer in using the DateBox control, Vogue Tools contains a "lex"-built tool that can be used to replace date-containing TextBox controls with DateBox controls. Typical processing time for an entire project (depending on size) is around 10 seconds. As a further example, to add the necessary exception handling template code to a routine, Vogue Tools contains an IDE Add-In that can do just that with one button click. The current Vogue Tools are listed here:

  • Line Number Wizard

  • Error Handler Inserter

  • Lex DateBox Parser

  • Egrep32

Structured Exception Handling Scenario

 As mentioned at the beginning of this appendix, all three components —VB Vogue, the App Template, and Vogue Tools—are designed to work together. Let's take a look at how this works with a Structured Exception Handling example.

VB Vogue mandates that each and every routine in a Visual Basic application must contain an exception handler. What that handler contains and how it should deal with errors is explained in VB Vogue. (For example, VB Vogue describes how exceptions should not only be caught, but how they should be logged.) However, VB Vogue does not include any of the necessary support and infrastructural code to actually do the next step. That part is delivered through the second component of the TMS Developer's Framework—App Template. Finally, Vogue Tools provides the Error Handler Inserter to insert the appropriate App Template code into the project.

How Are They Provided?

VB Vogue is currently provided as a Word 6.0/95 or Word 97 file. In the future it will be provided as a series of HTML files so that navigation can be achieved using a Web browser.

The App Template is provided as a single Visual Basic project in source code form: in other words, it is runnable. It also includes a .HLP file that contains product documentation. In the future this will be provided as a series of HTML files so that navigation can be achieved using a Web browser.

Vogue Tools are provided as built executables. Where appropriate to the running of the tool, source code is provided.

If you are interested in purchasing any or all of the TMS Developer's Framework Toolkit, please contact Sales@TheMandelbrotSet.com.


Appendix D

Visual DateScope 2000 and VBA2000

 The Mandelbrot Set (International) Limited (TMS) is the sole UK distributor and non-exclusive worldwide distributor of Visual DateScope 2000 (VDS2000) on behalf of Class Solutions Limited. VDS2000 is a developer's toolset designed to aid in the Year 2000 conversion of systems developed specifically in Microsoft Visual Basic.

Originally released in 1996, VDS2000 was the first product of its kind available worldwide, offering support for Microsoft Visual Basic Impact Analysis, Remediation, and Coverage Testing.

Unlike other so-called "generic" or "language-independent" products, VDS2000 has a built-in Visual Basic language parsing engine that understands the language entirely and doesn't just perform simple text searches and other basic tasks. As such, VDS2000 is much better equipped to locate and deal with Year 2000 date-challenged code.

Version 3.0 of VDS2000 builds on the success of previous versions by providing enhanced functionality and additional features (including eight new impact analysis reports), designed to minimize the time spent performing Year 2000 operations. Version 3 covers Visual Basic versions 3, 4, and 5. In addition, version 3 now supports an optional add-in called VBA2000 (developed in-house by TMS), which works directly with VBA source code imbedded within Microsoft Access databases and Microsoft Excel spreadsheets (in both Office 95 and Office 97).


Visual DateScope 2000 Fact File

 VDS2000 automates the tasks that would otherwise require manual intervention. Adopting an automated approach accelerates the conversion process, provides a framework for control, and reduces the risk of human error. The VDS2000 tool-set consists of two primary components, the Scanning Application Component and the Date Function Library Component. These components assist in date identification/impact analysis, renovation, testing, and debugging.

Date Identification Process

 The Scanning Application Component, consists of an executable application that performs an initial pre-scan through the Visual Basic or Excel/Access VBA source code extracting the declarations (or definitions) of all the main elements (tokens) that make up a Visual Basic project. You can add numerous Visual Basic projects to the catalog, allowing a repository and date scan of entire systems or of subsystems.

These tokens include the declaration of any controls, DLLs, constants, global variables, modular level and local variables, User Defined Type (UDT) names and element names and any function or procedure parameters. The Scanning Application Components also scans for the many intrinsic Visual Basic date functions and procedures, such as Now, DateDiff, and CVDate.

The Scanning Application Component provides database support by examining the internal structure of any ODBC or JET database, searching for date-related columns or fields and adding these to the repository.

Most date scanning applications will simply search program files for a predetermined set of date-related keywords. The Scanning Application Component recognizes the need for business domain knowledge by allowing the repository to be extended to include user-defined tokens. A scanning application that cross-references a fixed set of date-related keywords would run the risk of entirely missing a business domain date-related variable such as sEffectiveFrom.

A hierarchical repository display ensures that the user can work through the components of each project in a logical format and visualize the completion of each logical unit. Compare this with the chore of manually trudging through thousands of lines of code unaided and with no physical indication of work completed—chaos!

The Scanning Application Component provides a Results repository (sorted by module and procedure) of all date-related occurrences identified within each of the specified projects. You can print the repository or use it directly for renovation. (See the next section, "Renovation Process", for more details.) Using the repository of date occurrences, you can return to the Visual Basic project knowing each identified line of code in need of possible modification.

The Scanning Application Component provides an automatic update of the source code with annotations marking each of the identified date occurrences. By automating the majority of the Date Identification process, it is possible to increase accuracy because the risk of human error is reduced. The completion time for the whole process is a fraction of the time it takes to run a manual scan. Consider an average Visual Basic project consisting of 15,000 lines of code. Taking an average of four seconds per line, this could take up to two days to complete for the identification of just one application!

Renovation Process

Visual DateScope 2000 provides a structured and consistent approach to problem solving.

Having identified all date-type occurrences within a system, the process now turns to the renovation of those date occurrences. Using Visual DateScope 2000, you can modify the code directly from within the scanning application.

Renovations can take place without ever having to load Visual Basic, which provides a self-contained conversion environment. There is no context switching.

Year 2000 Library

Once the identification of dates is complete and a method is in place to modify those dates, the question arises as to what to do with the date occurrences. The Date Function Library Component in the Visual DateScope 2000 toolset consists of a library of Year 2000-related date functions designed to erase the threat of the Year 2000 problem in your Visual Basic code. The library has the following features:

  • Fully-tested functions appropriate for countless Year 2000 scenarios.

  • Consistent approach to fixing date code.

  • Completely generic, supporting international dates.

  • Handles incorrect input values that are not dates. The reason for this is simple: many companies will hire contract or part-time staff to help in their Year 2000 conversion projects. It is possible that these programmers have little experience in either your business domain or in the Visual Basic language itself. The Year 2000 library therefore tolerates incorrectly applied "fixes."

Testing

 Although VDS2000 does not test changes, it will certainly increase testing productivity and accuracy. For example, when changing a line of code from within the Scanning Application Component, VDS2000 will automatically add a line of debug code into your source code immediately before your change, if required. Comparative analysis ensures the complete execution of all changed lines of code. An additional debug testing file can be created that shows the "Before" and "After" values of converted dates.

All the debug calls inserted into your code can then be easily removed from within VDS2000 (for production environments).

Information Packs

Contact y2kservices@themandelbrotset.com for your information pack, which contains evaluation copies of both Visual DateScope 2000 and VBA2000.


Appendix E

Keeping Current with TMS

 The Mandelbrot Set (International) Limited (TMS) has arranged to distribute any code updates, book news, further Visual Basic 6 news, tips, errata, or any other book extras via a TMS mail-server newsgroup. Just in case you don't know, a mail-server newsgroup is essentially a machine (server) used to automatically send relevant e-mail to parties who have registered for a certain newsgroup subject.

If you would like to be kept up-to-date, please send e-mail* to TMS-List-Server@TheMandelbrotSet.com. Leave the subject line blank and insert this line anywhere in the body of your message:

Subscribe TMSBookList

This subscribes you to the TMSBookList newsgroup on the TMS mail server. Whenever we issue updates, news, and so on, we will distribute them via simultaneous e-mail to everyone who has subscribed to the TMSBookList newsgroup.

Should you wish to unsubscribe from the group at any time, send e-mail to the address given above, but this time insert this line anywhere in the body of your message:

Unsubscribe TMSBookList

You will receive confirmation e-mail back from the server when your communication has been successful. What could be easier?


Other Newsgroups

 If you would like to subscribe to any other TMS newsgroup, send e-mail to the address given above and insert this line anywhere in the body of your message:

Lists

The TMS mail server will then e-mail back to you a full list of the names of the public newsgroups to which you can subscribe (complete with a description of each newsgroup). Once you know the names of all the different newsgroups, you will be able to subscribe and unsubscribe to them as described in this appendix.