Designing and writing code is only a portion of the process of creating an application. Debugging prior to release can take more time and effort than most programmers are willing to admit—or schedule. Applications that provide objects can be especially problematic, because they expose a whole new (programmatic) interface with a whole new set of problems. This chapter discusses those problems and supplies approaches for solving them.
In this chapter, you learn how to do the following:
Debugging an application that uses objects internally or provides objects to other applications presents these new problems:
The following sections discuss these problems in greater detail and explain how you solve them.
When you try to place a watch on an object variable, Visual Basic displays the value “Object doesn't support this action” in the Watch window, as shown in figure 22.1.
Visual Basic cannot watch object variables directly.
This is a huge problem, because objects might have multiple instances and you often must know which instance you are dealing with when a bug occurs. To find an instance of an object, you must use a watch expression on some property of the object. When debugging objects, it helps if you’ve defined two standard properties:
Placing a watch expression on Object.Name tells you whether you’re looking at the correct object. Checking Object.Parent helps you look backward in code to discover how the object was created. Figure 22.2 shows how to use the Name and Parent properties in the Watch window when tracing execution.
Use object properties to determine the instance of an object.
The Name and Parent properties aren’t automatic for all objects. Programmers must define the properties in code. Although defining these properties for all objects might seem like extra work, the properties can save you much trouble when tracing through complex object interactions.
Visual Basic applications can have multiple instances of the same application loaded in memory at the same time. Visual Basic’s OLE Automation features, however, deal with only a single instance of any application. If you have multiple instance of an executable file, you can’t accurately predict from which instance Visual Basic will get an object, as shown in figure 22.3.
CreateObject and GetObject functions don't discern among multiple instances of an object's application.
After getting an object from multiple instances of an application, Visual Basic always seems to use the same instance. Therefore, you can’t enumerate through the instances until you find the one that you want.
The only sure way to avoid bugs from connecting to the wrong instance of an object’s application is to limit the object’s application to a single instance. Listing 22.1 shows how to use objects provided in the SYSTEM.VPJ, from this book’s companion CD, to check whether an application is already loaded in memory. Using these lines as the startup procedure of an application that provides objects prevents more than one instance of the application from loading.
Listing 22.1 SYSTEM.VBP Checking for Loaded Applications
You can’t depend on AppActivate returning an error if an application is not running; OLE Automation can start applications invisibly. You must use the Windows API calls contained in SYSTEM.VBP to detect applications started for OLE Automation reliably.
If you are already running an application that provides objects, Visual Basic’s CreateObject function might not start a new instance of the application. Instead, the function calls the code in the running instance of the executable file to create the new object. This can cause problems if the application initializes global object variables at startup. Figure 22.4 illustrates how this problem can occur.
If the object's application is already running, CreateObject does not run the Sub Main procedure.
In figure 22.4, the CreateObject function causes an error in the object’s application, because the object’s first Initialize event procedure released the gINI object the first time that it ran. The second Initialize event procedure can no longer get gINI, so an “Object variable not set” error occurs.
This problem isn’t limited to the use of CreateObject. Visual Basic actually has four ways to create a new instance of a Public, Creatable object within an already running application:
To avoid problems with initializing objects, don’t initialize an object’s dependent data outside of its Initialize event procedure unless you preserve that data for the life of the application. If possible, make each Public, Creatable object in your application self-contained.
In figure 22.4, both Application objects share the same address space. This can lead to other bugs, because both objects have access to the same global variables and functions. You can’t see from the other applications the global data in the object’s application, but you can affect the data through the object’s methods and properties, as shown in figure 22.5.
All the objects that an application provides share global variables.
By causing Object1 to change a global variable that Object2 uses, Application1 can inadvertently affect Application2. This problem underscores the importance of not using global variables or procedures in applications that provide objects. Objects that must share data with their subordinate objects should do so through instances of a Private object. Figure 22.6 illustrates how you use a Private object to share data among subordinate objects.
Create a Private data object instead of using global variables.
Using a Private data object ensures that each new top-level object has its own, private data that other top-level objects created by other applications will not affect.
Sometimes an object’s application can remain loaded invisibly in memory. This sometimes happens when a CreateObject or GetObject function succeeds in starting the application but fails before returning the reference to the object. This can result in a lost reference to the object; you can’t destroy the reference, because your object variable isn’t Set.
If a CreateObject or GetObject function fails while you are debugging an application, check the Windows task list to make sure that the object’s application isn’t left in memory. If it is, you can close the application from the Task List.
In compiled code, you should not call CreateObject or GetObject again when handling errors from these functions. Doing so might simply create more lost references and, eventually, fill available memory.
If you use the following form of GetObject, Visual Basic returns a reference to an existing instance of an object:
The object can’t detect that more than one application has access to it, although OLE increments its reference count and keeps the object from being destroyed until the last reference goes out of scope or is set to Nothing.
Therefore, one application can affect another through a shared object. Sometimes you want this is to happen. However, in other instances, it causes unexpected results.
Because an object has no built-in knowledge of which application will use it, you either need to trust that other programmers will be careful when getting running instances of objects, or you must to limit access to subordinate objects through some sort of password mechanism. The following code shows a method that returns a subordinate object only when the appropriate KeyValue argument is provided:
When creating OLE Automation objects during debugging, Visual Basic seems to prefer compiled versions of the object over the object’s project loaded in another instance of Visual Basic. During debugging, an application might actually have two entries in the system registration database, as shown in figure 22.7.
The compiled version of the object exists at the root level; the debug version exists under the VBKeySave entry.
The compiled version of the object exists at the root level; the debug version exists under the VBKeySave entry.
When attempting to create the object, another application searches for the object in this order:
To ensure that you are using the right object when debugging, close all instances of the object’s application. Make sure to check the Windows Task List to ensure that no hidden instances of the application are running. Then start the correct version of the object’s application in another instance of Visual Basic.
As an added precaution, avoid creating an executable file for an object until you have completely debugged it. This saves you much work maintaining correct system registration database entries on your system.
Approaches to debugging are as varied and personal as driving habits. Some programmers debug their applications defensively and others are more aggressive. The following list constitutes the “rules of the road” for debugging applications that provide objects, but, like the rules of the paved road, they are subject to interpretation:
When designing an application that provides objects, consider starting it from a Sub Main procedure rather than from a form. In addition to helping you think clearly about how the application starts up, this technique gives the application a place to add some conditional code that tests the application in-process, whether it has a visual interface or not.
Because applications that provide objects shouldn’t use global variables or procedures or initialize dependent objects outside of a class’ Initialize event procedure, you should dedicate almost all the code in your Sub Main procedures to debugging the application in-process, as shown in Listing 22.2, which is from the companion CD’s OUTLINE.VPJ sample.
Listing 22.2 Use a Main Procedure for In-Process Testing
The #Const DebugBuild = -1 setting at the beginning of the module includes all the subsequent debugging code. The code in Sub Main and the AddLevel and SearchTree procedures are designed to go through all the possible code paths for the Topic object. Placing this code in Sub Main rather than doing ad hoc testing builds a consistent debugging path that you can use cross-process and for platform testing later.
When you compile the release version of the executable file, change the DebugBuild option to 0. This prevents the debug code from being built in to the executable file, and thus saves code space.
To debug an object for cross-process access, follow these steps:
The StartMode options of the Options properties pages have an effect only while debugging an application within Visual Basic.
The Error Trapping options of the Advanced Options property page determine which application Visual Basic stops in if an error occurs.
As you debug an object’s application, test the logic of your objects under different access paths. One of the most critical areas for objects is their initialization. Therefore, it is important to test the different forms of access that can result in different types of initialization.
In addition to run-time errors, you must watch for unexpected results that can result from uninitialized data or from multiple initialization of the same object. Comparing expected results to actual results is important.
Creating New Objects from a Running Application
If you created debug code in Sub Main while debugging in-process, you can use very similar code to debug cross-process access. Listing 22.3 contains procedures that show the modifications made to the in-process debug code for OUTLINE.VBP.
Listing 22.3 Debugging OUTLINE.VBP Cross-Process
The test code in listing 22.3 uses the generic Object data type and CreateObject to avoid having to establish a reference to the object’s application. References to the object’s application type library aren’t available until the application is compiled. You should not compile object applications before debugging them for cross-process access, because creating an executable file results in registering with your system two versions of the applications.
Getting Existing Objects from a Running Application
To ensure that the way that you get an existing object from a running application works correctly, add a new procedure call before the end of your cross-process-debug Sub Main. The new procedure, GetRunningObject (see listing 22.4), should use GetObject to manipulate the object that you created earlier.
Listing 22.4 The GetRunningObject Procedure
The GetRunningObject procedure uses the GetObject function, rather than a passed-in variable, to get an existing object reference. This tests the code path for returning and modifying an existing object.
Starting the Object’s Application
You should test starting the application by using CreateObject, because by doing so you run the object application’s Sub Main procedure. Using CreateObject with the object already running doesn’t test this path for cross-process access.
The Visual Basic documentation implies that you can start an object’s application for cross-process debugging by using CreateObject. Currently, that technique doesn’t work. You must first start the object’s application in Visual Basic before attempting to use CreateObject.
If the documentation is correct, and the product is fixed by release time, you should be able to simply stop the application started in step 5 in the preceding section, then run the cross-process test again.
However, if the documentation is wrong, you must use a compiled version of the object’s application to test whether CreateObject correctly starts the object’s application. Be sure to register the object’s application before testing with the compiled application.
You can run as many instances of Visual Basic as your machine’s memory can hold. Therefore, you can debug an object’s application using two or more accessing applications at the same time. You can’t have more than two object applications running in instances of Visual Basic, however. This is because the VBKeySave entry that Visual Basic creates in the system registration database is limited to two applications.
You can use the same debug code that you used for cross-process debugging when debugging multiple access, although it is better to change the order in which object’s are used among the accessing projects. Having different orders of access is more likely to uncover conflicts among objects running multiple cross-process access down parallel paths.
After completing the preceding steps, you can build your debug executable file to start testing for errors on target platforms. You should not rely on debugging performed on developer’s machines as a final test, because developers tend to have more available memory and faster processors than many of the users for whom they develop.
Try to get a couple unused machines that are representative of those used by the users for whom you develop. You might be surprised at the diversity of problems that can reveal themselves when running the same code on computers from different manufacturers. Some manufacturers are more fastidious than others at following standards and doing quality checks. You’ll soon learn which types of machines, video cards, and disk drives are “tolerant” and which ones aren’t.
If you’ve followed the steps in this chapter to this point, you probably have a pretty good code base for creating some automated testing on these platforms. If not, you should return to the section “Strategies for Debugging Objects” and start over. When testing on target platforms, you should use compiled objects and test code. If a problem develops on a particular machine, you can install Visual Basic and step through the code to find the error.
Testing on target platforms also gives you a good chance to test your Setup procedure. Object applications must be properly registered in the system registration database. As this book has frequently mentioned, getting the registration right can pose problems.
After releasing an application that provides objects, you have a whole new problem: maintenance. Some applications just keep chugging away like old Volvos. Others seem more like MGs. Whichever type of application you’re responsible for, you don’t want to kill the driver every time that you make a few changes under the hood.
Visual Basic gives you a handy feature for maintaining compatibility with released object libraries: the Compatible Object Application text box on the Options properties pages (see fig. 22.10).
Type the name of the released executable file to which to maintain compatibility in the Compatible OLE Server text box.
Entering a name in the Compatible OLE Server text box compares the loaded application to the existing executable file’s object library when you run or compile your application. If you’ve broken compatibility with the released version, you get a warning message.
These types of changes cause a warning to occur, because they break compatibility with released object libraries:
These aren’t the only types of changes that can break compatibility with released object libraries. Changes in behavior, memory requirements, or software requirements can be just as deadly.
You should maintain guidelines for naming variables, procedures, constants, and other user-created items in the code that you write. Naming these items in a consistent way helps you distinguish among Visual Basic keywords and items defined somewhere in code. Such guidelines are especially useful in large programs or those in which more than one user uses the code.
At first, following a set of guidelines seems to require much extra work. It quickly becomes second-nature, however. Style guidelines not only make it easier for other programmers to read your code, they also make it easier for you to remember what you wrote. Also, following guidelines can give your work a professional-looking polish. Other programmers will be jealous when they see how consistently you can produce bug-free software by doing some simple housekeeping.
The guidelines that this section presents are a good starting point and are used throughout this book in the sample code. You might choose to develop your own or follow someone else’s. Only a couple rules are absolute:
Visual Basic enables you to work in a very free-form way. You can start writing code without ever worrying about reserving space for variables or assigning the correct data types. This is a great way to start learning and enables you to begin doing useful work almost immediately. However, it also means that typos in variable names are very hard to catch.
For example, the following code does not display the message box, because the If statement misspells bAnswer. Visual Basic considers Anwser a new variable and initializes it to an empty value.
If you add an Option Explicit statement to the beginning of each module, form, and class, Visual Basic checks to ensure that you have declared each variable that you use. This is an effective way to check the spelling of variable names throughout your code.
Here's the same code with an Option Explicit statement:
You can still have typos, but Visual Basic alerts you when you try to run the code.
Using Option Explicit is more than good form; many contracts that request software written in Visual Basic require the use of the statement.
Scope is the range within code where you can use a variable, constant, or procedure. Visual Basic has three levels of scope:
Scope | Description | Prefix |
Local | The variable or constant is valid only within the procedure that defines it. | None |
Module | The variable or constant is valid only within the module that defines it. | m |
Global | The variable or constant is valid within any module in a project. | g |
Any variable or constant defined in a procedure has local scope by default, as in the following example:
If another procedure uses the variable iCount, the variable is new and doesn’t reflect the value from the procedure LocalScope. You cannot change local variables outside the procedure.
Constants behave the same way, except that their values never change (hence their name). Note the following example:
Again, other procedures can’t see the value of RED defined in the procedure LocalScope.
The names of constants are usually typed in uppercase characters. The exception to this rule is a product-defined constant that begins with a product prefix, as in xlMaximize.
When using local variables, the Dim statement is optional. Visual Basic automatically creates a variable whenever you use a word that it does not recognize, as in the following example.
When creating a variable automatically, Visual Basic uses the default data type. Visual Basic uses the Variant data type as the default unless you change it by using the Deftype statement.
Automatic variables make it hard to catch variable name spelling errors. It is safer to turn off this feature by using the Option Explicit statement.
Any variable or constant defined outside a procedure has module scope by default. Listing 22.5 shows such a procedure.
Listing 22.5 A Procedure with Module Scope
The ModuleVariable and DisplayFlag procedures can see and change the mbFlag variable. If you move DisplayFlag to another module, however, this is no longer true; the procedures can see the value mbFlag only within the module that declares it.
Constants behave the same way. If you declare them outside a procedure, every procedure in that module can see them.
Variables and constants defined outside a procedure with the Public keyword have global scope, as in the following examples:
The variable gbFlag and the constant gRED are available to all procedures in all modules. Notice that you can’t use Public inside a procedure; this keeps global and module scope declarations together at the beginning of each module.
Data type indicates the kinds of data that a variable can contain. Visual Basic has 12 built-in data types, which table 22.1 lists.
Table 22.1 Recommended Visual Basic Fundamental Type Prefixes
Data Type | Prefix |
Boolean | b |
Byte | by |
Currency | c |
Date/time | dt |
Double | d |
Error | err |
Integer | i |
Long | l |
Object | obj |
Single | s |
String | str |
User-defined type | u |
Variant | v |
In addition to the built-in data types, Visual Basic has many types for objects and collections. It is important to use a set of prefixes for these object types and for the types of objects that you create in your own application. Table 22.2 lists recommended prefixes for the Visual Basic object types.
Table 22.2 Recommended Visual Basic Object Variable Prefixes
Object Type | Object Prefix |
Form | frm |
MDIForm | mdi |
CheckBox | chk |
ComboBox | cbo |
CommandButton | cmd |
CommonDialog | dlg |
Data | dat |
DBCombo | dbc |
DBList | dbl |
DBGrid | dbg |
DirListBox | dir |
DriveListBox | drv |
FileListBox | fil |
Frame | fra |
HScrollBar | hsb |
Image | img |
Label | lbl |
Line | lin |
ListBox | lst |
Menu | mnu |
OLE | ole |
OptionButton | opt |
PictureBox | pic |
Shape | shp |
TextBox | txt |
Timer | tmr |
VScrollBar | vsb |
The names of variables, constants, and procedures should tell you something about them. This is especially important for procedures and for variables that refer to objects or have module or global scope.
For example, if you have a variable that refers to a button with the caption OK, you should name the variable cmdOK. A procedure that sorts arrays should be named SortArray.
Using shorter names for frequently used items is convenient. For example, iCount is a convenient name for a counter in a For...Next loop. Many programmers simply use i or j for counters in For...Next loops.
If a name seems too long, being less descriptive is better than using a series of abbreviations. Wherever you declare the item, you can always add comments to describe fully the item's use.
Difficulty naming a procedure is sometimes a good tip-off that the code is too general or complex. You might consider breaking a procedure into several smaller ones if you can't come up with a descriptive name.
Table 22.3 presents some guidelines for naming procedures.
Table 22.3 Procedure-Naming Guidelines
Good Name | Poor Name | Reason |
iCount, i, j | IndexvCounter, LoopCounter | Counters that you use in For...Next and Do...Loop statement blocks should be easy to type and identify. Using Integer variables makes loops execute faster. |
SortArray,Display Result, AdjustMargins | DoThings, ShowIt,FormatOutputFor Printing | Procedure names should be descriptive and specific. Using "It" or "Thing" is far too general. Try to use verb/noun pairs. |
butOK,chrtInventory, rngSourceData | Button1, chrtInv, rMyData | You should name object variables consistently and cleary identify what they represent. If an object has a descriptive caption or name, use that name as part of the variable name. |
Use tabs to indicate the relationships among blocks of code. For constructions with a beginning and end, such as loops, indent the contents of the construction once for each level of nesting. Listing 22.6 shows a well-indented code block.
Listing 22.6 A Well-Indented Code Block
In listing 22.6, you can easily see where loops and conditional statements begin and end. This is critical in long passages of conditional code, such as long Select Case statements or a series of If...Then statements.
Good comments are the most important step to writing code that other programmers can understand and maintain. This practice is extremely important if you want to take occasional vacations from work. Try writing comments one procedure at a time, using the following general form:
Listing 22.7 shows a block of code that demonstrates these commenting guidelines.
Listing 22.7 A Well-Commented Code Block
For more information on the following topics, see the indicated chapters:
© 1996, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company.