Avoid Programming Pitfalls

Learn how to deal with the Top 10 mistakes
even the most experienced developers make.

by Keith Pleas

Reprinted with permission from Visual Basic Programmer's Journal, 3/98, Volume 8, Issue 3, Copyright 1998, Fawcette Technical Publications, Palo Alto, CA, USA. To subscribe, call 1-800-848-5523, 650-833-7100, visit www.vbpj.com, or visit The Development Exchange at www.devx.com

As Visual Basic has grown through the years, gaining features and becoming more complex, the most common programming mistakes have evolved in a similar fashion. We all made plenty of mistakes in the old days of VB 1.0 through 3.0 (see the sidebar, "Mistakes from the Early Days"). Of course, it's still possible to make mistakes today. But the average skill level of VB developers has increased as they spend more time with the product and come into contact with more features. For instance, the majority of VB applications developed today use some form of data access, and many make use of in-process ActiveX servers and controls. I'll show you how to avoid the Top 10 VB5 programming pitfalls. After that, I'll give you my opinion on the Top 10 mistakes Microsoft made with VB5 (see the sidebar, "Not All of Them Are Your Fault").

Mistakes from the Early Days
Programmers have always made mistakes with Visual Basic. Let's take a brief look back to the early days of VB in versions 1.0 through 3.0. While experienced VB developers might quibble about one or two items, I suspect most would come up with this list of Top 10 programming mistakes:
  1. Recursion, most often in events, resulting in "Out of stack space."
  2. Cutting a control, which relocates code to the general Declarations section.
  3. Inadvertently using variants, often by declaring multiple variables in a single statement.
  4. Concatenating strings with + (instead of &) and having the strings evaluated as numbers.
  5. Setting properties at design time instead of setting them in code.
  6. Hard-coding design-time paths-this is particularly unfortunate with the Data control.
  7. No error checking, which causes a runtime error to pop up a message box and then terminate the app.
  8. Bad DLL declarations that cause various undesirable results.
  9. Because the Setup Wizard had problems, creating a Setup using BAT files that place common components in incorrect places, such as the Windows directory.
  10. Not using the correct data type-most commonly, not using Constants and overflowing Integers.
K.P.


Not All of Them Are Your Fault
Everybody has a favorite feature they just know Microsoft should put in VB to make the product immensely better. Or they can name a feature that should be changed. Personally, I think I must hate VB's Menu Editor (carried over from VB1!) more than anyone else. But neglecting to add a major feature, such as a new Menu Editor, isn't quite as bad as doing something wrong to begin with. Here's my list of the Top 10 mistakes Microsoft made with VB5:

  1. No option to turn off LastDLLError. This probably causes a 15 to 20 percent performance hit.
  2. Not using Enums religiously in VB and VBA properties so they appear in statement completion and the property browser.
  3. Not including all the interfaces required by using Implements.
  4. Trying to hide the complexity of binary compatibility. VB5 adds new Interface IDs every time it compiles instead of replacing them if the names stayed the same. This is why you must maintain a separate version for compatibility.
  5. No smart renaming of controls and code. Closely related is not copying controls and code to the clipboard.
  6. Extreme difficulty in turning an OCX inside a project group into a standalone and then using it.
  7. In UserControls, not allowing access to runtime properties of wrapped controls. For example, you can't set the Sorted property of a ListBox runtime inside a UserControl. This could have been mitigated somewhat if Microsoft had maintained the API for destroying and re-creating controls that were available in VB 1.0 through 3.0.
  8. No "Declare Unicode," which would not do API string munging. VB internally knows how to avoid this because it already does it if the function is declared in a type library. To pass a Unicode string, you can declare the parameter ByVal as Long and pass the string variable with the undocumented StrPtr wrapper function.
  9. Not allowing VB applications to specify Return (or exit) codes.
  10. Not providing separate native compilation options on a per-module basis.
The last item on my list triggered a personal crusade to hack into the native code compilation process to see if I could create OBJ files with different settings and then successfully link them together into a carefully tailored executable. As you might have discovered, VB ships with a slightly customized version of the second pass compiler C2.EXE from VC++, along with its own LINK.EXE. My first task was to intercept the calls made to C2 and LINK and examine them. I did this by creating a shim utility, which I could rename to whatever executable I chose, that simply logged the Command$ string to the Registry and then called the original executable. (I renamed the executable by prepending the string "VB"). This was quite easy and logged these outputs for calls to C2 and LINK, respectively:

The VB Application identified by the event source logged this Application Project1: Thread ID: 220 ,Logged: -il "C:\TEMP\VB330857" -f "Module1" -W 3 -Gy -G5 -Gs4096 -dos -Zl -Fo"C:\vb5\Module1.OBJ" -QIfdiv -ML ---basic.

The VB Application identified by the event source logged this Application Project1: Thread ID: 192 ,Logged: "C:\vb5\VBSnarf.OBJ" "C:\vb5\ Module1.OBJ" "C:\vb5\vbsnarf.OBJ" "C:\vb5\VBAEXE5.LIB" /ENTRY: _vbaS /OUT:"C:\vb5\vbsnarf.exe" /BASE: 0x400000 /SUBSYSTEM:WINDOWS, 4.0 /VERSION:1.0/INCREMENTAL:NO /OPT:REF /MERGE:.rdata=.text /IGNORE:4078

Fortunately, while VB was waiting for C2 or LINK to return, all the temporary files were available for me to archive off (this is getting exciting, isn't it?). Then I decided to address number 9 on my list and find a way to grab the exit code from whatever executable I called and make that my exit code, carefully mimicking the app I was impersonating. This involved using only five Win32 APIs (see Listing 1, page 45). Although it also involves using several structures (see Listing 2, page 46), it turns out that STARTUPINFO (the scary one) only requires that the length be set and all other parameters be zero (or C NULL).

With the shim utility now working perfectly, I added a MsgBox statement to get it to pause long enough to copy the files. Now comes the results, which are somewhat mixed: although I was able to substitute in class modules with different compilation options, substituting either a form or code module generated a string of "unresolved external symbol" errors when attempting to link.

—K.P.


1. DECLARE OBJECTS EARLY BOUND.
Almost everyone knows that early binding is better, even if they're not quite sure why. Other than the increased speed in object access (like an order of magnitude), errors are reduced through early syntax checking. There is additional support in the development environment for features such as statement completion. What many developers fail to understand, however, is that how an object is declared is the only thing that determines whether an object is early bound. Object creation (using, for instance, the CreateObject statement) has nothing to do with it. If you declare an object "as Object", that object is late bound. If you declare it as a specific object type-by obtaining a reference to the type library and declaring it "as rdoConnection"-you're declaring it as early bound.

2. DON'T USE GETLASTERROR TO DETERMINE ERRORS.
Although bad DLL declarations are still common, the prototypes supplied by Microsoft have definitely improved in quality (and quantity) over the past few releases of VB. It's still necessary to check everything scrupulously-parameters and return values in particular-but the new API-related problem has to do with errors. (For more information, see my OLE Expert column, "Get a Handle on OLE Errors," in the April 1996 issue of VBPJ.) When API calls fail, they return zero, and it's up to the programmer to figure out why. The Win32 API function GetLastError tells you what error occurred, but a call to this function isn't guaranteed to be the next statement executed, so the result might not accurately reflect the error. Instead, VB caches this value in the LastDLLError property of the Err object each time an external DLL function is called. Do not use the GetLastError function in any VB code.

3. SET PROPERTIES IN CODE AT RUN TIME.
Another problem that's been around since the beginning is hard-coded properties. It's seductively easy to set properties in the design environment instead of in code at run time. Microsoft even added the Form Layout window to VB5 to make it "easier" to position a form at startup. Hey, if you can't write one line of code, such as this code that centers a form, then find another product to develop with and keep your apps off my desktop:

Me.Move (Screen.Width - Me.Width) / 2, _ (Screen.Height - Me.Height) / 2

Of course, if you want to center multiple forms (or write more reusable code), you can wrap this line in a procedure:

Sub Center(frm As Form) frm.Move (Screen.Width - _ frm.Width) / 2, (Screen._ Height - frm.Height) / 2 End Sub

Simply call this procedure from inside a form with this code:

Center Me

The worst properties by far to set at design time are the Data control's DatabaseName and-to a lesser extent-RecordSource. The reason? If the database isn't on every machine on which you run your app, your app will fail on those machines before it gets a chance to execute a single line of code.

A similar but mostly benign situation arises when developers stuff graphics into their apps. Although it should be obvious that the Picture property of a form or control contains a graphic, it's easy to lose track of smaller graphics that have been stuffed into Icon, MouseIcon, and DragIcon properties. Hard-coding the application's Help file is a mistake that occurs less frequently because many developers don't bother to document their apps. Those that do might not have run across the Help File Name setting in the Project Properties dialog. Instead, you should get in the habit of setting the App.Helpfile property at run time.

4. DON'T CONFUSE NULL, NOTHING, "", EMPTY, AND ZERO.
There are many ways in VB to say that something is "nil," a term I'm going to use temporarily to collectively indicate zero, uninitialized, blank, empty, and so on. Each of these terms is subtly different. For instance, it might seem picky to distinguish between an empty string, an uninitialized variable, an ASCII zero, and a null pointer. But these differences affect many things, from database storage to calling external functions. Complicating the situation is that other languages-C in particular-often handle similar constructs differently.

Sometimes VB knows what's right and raises an error when it encounters code that tries to assign Null to a string:

Dim sName as String sName = Null

But more often, a programmer tries to find out if something is "nil" by testing it against a known value (zero, "", False, or a constant) or using a built-in testing function such as IsEmpty or IsNull. Sometimes the results are not what was expected. For example, setting an object pointer to Nothing is not the same as making it null. The test in this code will always return False:

Dim obj as Object Set obj = Nothing If IsNull(obj) = True Then…

And, of course, the only thing possibly worse than buggy code is getting flamed for using the wrong term in public. To keep from being embarrassed, you should know these terms:

  • "": A zero-length string (commonly called an "empty string") is technically a zero-length BSTR that actually uses six bytes of memory. In general, you should use the constant vbNullString instead, particularly when calling external DLL procedures.
  • Empty: A variant of VarType 0 (vbEmpty) that has not yet been initialized. Test whether it is "nil" using the IsEmpty function.
  • Nothing: Destroys an object reference using the Set statement. Test whether it is "nil" using the Is operator:

    If obj Is Nothing Then...

  • Null: A variant of VarType 1 (vbNull) that means "no valid data" and generally indicates a database field with no value. Don't confuse this with a C NULL, which indicates zero. Test whether it is "nil" using the IsNull function.
  • vbNullChar: A character having a value of zero. It is commonly used for adding a C NULL to a string or for filling a fixed-length string with zeroes:

    Path = String(255, vbNullChar)

  • vbNullString: A string having a value of zero, such as a C NULL, that takes no memory. Use this string for calling external procedures looking for a null pointer to a string. To distinguish between vbNullString and "", use the VBA StrPtr function: StrPtr(vbNullString) is zero, while StrPtr("") is a nonzero memory address.

Listing 1
VB5

Sub Main()
Dim lResult As Long
   Dim proc As PROCESS_INFORMATION
   Dim start As STARTUPINFO

   App.LogEvent Command$, vbLogEventTypeInformation

   start.cb = Len(start)
   sCmd = App.Path & "\VB" & App.EXEName & " " & Command$
   If CreateProcess(vbNullString, sCmd, ByVal 0&, ByVal _
      0&, APITRUE, NORMAL_PRIORITY_CLASS, 0&, _
      vbNullString, start, proc) Then
      lResult = CloseHandle(proc.hThread)
      lResult = WaitForSingleObject(proc.hProcess, _
         INFINITE)
      lResult = GetExitCodeProcess(proc.hProcess, iExit)
      lResult = CloseHandle(proc.hProcess)
      ExitProcess iExit
   Else
      MsgBox Err.LastDllError
   End If

End Sub
Listing 1 Working with the Command String. This intercepts and logs a command string and then processes it, passing along the exit code.
5. USE PROPER PROCEDURES DURING SETUP.
With today's componentized applications, using a commercial setup program is mandatory. But even the smartest program can't help you if you don't set the version number of your components properly. You need to use the Make tab of the Project Properties dialog, and maintain compatibility with previous versions by using the Component tab of that same dialog. Equally important is testing on a "clean" machine. However, finding such a beast can be difficult, and after the first install attempt, it isn't "clean" anymore.

Another common mistake is distributing previous versions of VB5 CAB files with your application. If you absolutely must ship the CAB files, at least get the latest from the Microsoft Web site. Use the Setup Wizard to point to the latest CABs, then swipe the URLs from the created INF file and download them separately.

6. DON'T CREATE BAD PROGRAMMATIC INTERFACES.
An object's programmatic interface consists of the properties, methods, and events that it exposes. We've all looked at poorly designed objects with stupid interfaces: sometimes these objects have even been developed by people other than ourselves! Because properties are the simplest interface element, let's look at three simple things you can do to make them easier for developers to use.

First, it's often desirable for a property, such as a runtime-only property, not to appear in the property browser, or for obsolete or reserved interfaces to appear in the Object Browser. However, a property still must reside here for compatibility purposes. Using the Advanced portion of the Procedure Attributes dialog, you can hide interfaces by selecting the "Hide this member" and "Don't show in Property Browser" options.

Next, you're probably accustomed to using default properties, but many developers neglect to specify them for the objects they create. In the Advanced portion of the Procedure Attributes dialog, you need to set the Procedure ID to Default. At the same time, you might want to select the "User Interface Default" option to set the event displayed when the user double-clicks on the control. You should do this for properties but not for methods. Doing this also determines which property is highlighted in the property browser when you select the control.

Finally-and this is something that even Microsoft neglected to do in many cases-you should use Enums for property values wherever possible. Enums give you the list of possible values in both the property browser and in the development environment's popup completion. And they're easy to code. For instance, to specify an Alignment property for a control as having Left, Right, and Center values, you merely need to create a Public Enum for the constants and use that data type in the Property Get and Let statements:

Public Enum Align Left = 0 Right = 1 Center = 2 End Enum Public Property Get Alignment() _ As Align Public Property Let Alignment( _ NewAlignAs Align)

Note: explicit assignments are shown for clarity. The Enum values start at zero and, by default, increment by one.

Listing 2
VB5

Public Const INFINITE = &HFFFF
Public Const NORMAL_PRIORITY_CLASS =&b0
Public Const APITRUE = 1

Declare Function CreateProcess Lib "kernel32" Alias _
	"CreateProcessA" (ByVal lpApplicationName As String, _
	ByVal lpCommandLine As String, _
	lpProcessAttributes As Any, _
	lpThreadAttributes As Any, _
	ByVal bInheritHandles As Long, _
	ByVal dwCreationFlags As Long, _
	lpEnvironment As Long, _
	ByVal lpCurrentDriectory As String, _
	lpStartupInfo As STARTUPINFO, _
	lpProcessInformation As PROCESS_INFORMATION) As Long
Declare Function CloseHandle Lib "kernel32" (ByVal _
	hObject As Long) As Long
Declare Function WaitForSingleObject Lib "kernel32" _
	(ByVal hHandle As Long, ByVal dwMilliseconds _
	As Long) As Long
Declare Function GetExitCodeProcess Lib "kernel32" _
	(ByVal hProcess As Long, lpExitCode As Long) As Long
Declare Sub ExitProcess Lib "kernel32" _
	(ByVal uExitCode As Long)
Type STARTUPINFO
	cb As Long
	lpReserved As String
	lpDesktop As String
	lpTitle As String
	dwX As Long
	dwY As Long
	dwXSize As Long
	dwYSize As Long
	dwXCountChars As Long
	dwYCountChars As Long
	dwFillAttribute As Long
	dwFlags As Long
	wShowWindow As Long
	lpReserved2 As Long
	hStdInput As Long
	hStdOutput As Long
	hStdError As Long
End Type
Type PROCESS_INFORMATION
	hProcess As Long
	hThread As Long
	dwProcessId As Long
	dwThreadId As Long
End Type
Listing 2 Start Another Process. These Windows API declarations for Listing 1 handle starting another process and generating a return code.

7. IF IT'S NOT USED, TAKE IT OUT.
Leaving in unused code, controls, declarations, and references is still a problem. Because controls no longer appear in the Project Explorer, it's even easier to create something that works great on your machine but is either unnecessarily large or, if you're just distributing the source, fails to run on other developers' workstations that don't have the same (unused) components. The Project Components and Project References dialogs help, but they don't show everything. Do yourself a favor and look through your project's VBP file. You might be surprised at what's actually in your project and, because the VBP file shows the path, where the pieces are coming from.

8. KEEP A "COMPATIBILITY" FILE.
VB developers have long been in the habit of compiling EXEs on top of existing EXEs. Developers mostly did this for convenience, and it wasn't much of a problem. However, starting with VB 4.0-and the ability to create ActiveX DLL and EXE servers-developers were given the option of specifying a "compatible" reference version. VB checks this version each time it compiles the project to make sure the developers haven't changed the public "interfaces."

The "Using Binary Version Compatibility" topic in VB 5.0's Books Online hints at what's really going on and suggests using a separate "compatibility" file to prevent cluttering up your executable with interface identifiers. Here's what's really happening: each time you compile your ActiveX project (including, with VB 5.0, ActiveX controls), VB creates these IDs:

  • An ID for the typelib.
  • A CLSID for each creatable class in the typelib.
  • An InterfaceID (IID) for each Public class in the typelib, and a second one for each class that raises events.
  • An ID for each member (property, method, or event) of each class.

    The IDs for everything except the member ID are either matched to the "compatibility" file or, if they aren't in the compatibility file, generated randomly. The member IDs-again, those that aren't in the "compatibility" file-depend on their position in the code module, unless you override them by specifying the Procedure ID using the Procedure Attributes dialog.

    If the Version Compatibility is set to "Project Compatibility" using the Project Properties dialog, then the only ID enforced is the one for the typelib. Everything else can change randomly, although the member IDs will tend to remain the same if they haven't changed their relative position in the source code.

    If the Version Compatibility is set to "Binary Compatibility," then a lot more happens. In particular, a new IID is generated each time the object's interface changes. For compatibility, however, the previous IID is kept with the component and is marked to forward to the new IID. Every time the reference compatibility file is updated, the chain of IIDs grows for each creatable class. These additional IIDs take up 16 bytes each. More importantly, though, they result in extra registry entries and, in remote object scenarios, additional network roundtrips that reduce performance.

    If you're building ActiveX projects, you need to minimize the number of times your compatibility file changes. Your file typically changes when you "release" a version of your object server. If you haven't yet released your server, you should compile the server project and set up a "compatibility" file. Then you should set the server project for Project Compatibility and direct it to that file. Every time the server gets recompiled, recompile all clients.

    After you've "released" a version of your server and other people have started using it, you should set the server project for Binary Compatibility. If you change the server's interfaces when you recompile the server, you must recompile all clients that use those interfaces. You don't have to recompile the clients if you haven't changed interfaces or if no clients are using those public interfaces yet. Update the compatibility file so you won't have to recompile the clients again next time you compile the server.

    You should use the same name for the compatibility file and change the extension on a consistent basis. If you're sure your users won't have intermediate beta or test versions of your components, you can maintain copies of your major version compatibility files only and do a final full project compile before release.

    9. USE DISCONNECTED RECORDSETS WHERE APPROPRIATE.
    RDO 2.0 includes a new "Client Batch" cursor library that permits developers to disconnect an rdoResultset object from the rdoConnection object. The dissociated rdoResultset object acts as an updatable temporary static snapshot. You can then reconnect the rdoResultset object to any rdoConnection object and synchronize it with a remote database. Disconnected recordsets reduce server contention for database connections.

    ADO 1.5, which includes the same high-performance client-side cursor engine as RDO, now provides the same functionality through its Remote Data Services (RDS), previously named the Advanced Data Control (ADC). (You can find more information on ADO 1.5 at http://www.microsoft.com/data.) The RDS data control can open and populate a disconnected recordset asynchronously. It supports SQL Server, Access, Oracle, and other databases, while the ADC only supported SQL Server. Used in Web clients, RDS reduces server connection contention and allows the client to do data manipulation, such as sorting, without involving the server.

    10. UNDERSTAND ODBC CONNECTIONS.
    There are probably an infinite number of mistakes made with data access. Here are a few of the most common. If your application is running as a service or is being created by a service, such as IIS's ASP, you need to use a system data-source name (DSN) because services don't have a currently logged-on user. If you do not specify a database when you create a SQL Server DSN, everything ends up in the master database, which is probably not where you wanted it to go. Of course, you can also avoid DSNs entirely by creating "DSN-less" connections that specify everything in the Connect argument of the OpenConnection method. Aside from simplifying setup and administration (particularly where the data source is determined at run time), DSN-less connections can be slightly faster and give you more control over security.

    In this article, I've addressed the 10 most common programming mistakes made with VB5. The number of errors you make will probably correlate directly with the amount of time you spend with VB. And just think, once you get these mastered, it will be time to move on to VB6.


    Keith Pleas is an independent developer, writer, and trainer in Bellevue, Washington. He developed Microsoft's Professional Certification exam for Visual Basic. Reach Keith at keithp@curlew.com.