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.



Ltd Mandelbrot Set International Advanced Microsoft Visual Basics 6. 0
Advanced Microsoft Visual Basic (Mps)
ISBN: 1572318937
EAN: 2147483647
Year: 1997
Pages: 168

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net