17 Steps to Better
VBA Code
by
Dan Haught, Vice President of Product Development
September 11, 1997
http://www.fmsinc.com
Introduction
Writing
code is easy. Writing good code is hard. So what is good
code anyway? Generally, we think of the following
attributes:
- The
code works (no bugs)
- The
code is documented
- The
code is maintainable
- The
code runs quickly
Unfortunately,
meeting these goals is often time-consuming and
difficult. To help with this process, we at FMS have
compiled a list of things that we use on a daily
basistips and techniques that make it easier for us
(and you) to write code that comes closer the lofty goal
of defect-free maintainable code.
Coding
style and practices are a heated topic among developers,
approaching an almost religious fervor. The contents of
this article should not be construed as the final
wordrather, it is a exposition of the standards we
at FMS have found useful in the job of writing code.
There are guidelines contained here that you may not
agree with, which is fine; don't follow them. However, it
is important to understand the concepts involved, so
lets dive right in.
- Use Option Explicit
- Declare
Variables Correctly
- Avoid Variants
- Don't Use
Type Declaration Characters
- Use Narrow
Variable Scoping
- Convert
Data Types Explicitly
- Explicitly
Type Cast Function Return Values
- Implement
Robust Error Handling
- Pay
Attention to Program Control
- Use
Constants To Avoid Hard-coded Values
- Use
Variable Naming Conventions
- Choose
Meaningful Variable Names
- Add Comments
- Use
Standard Indentation
- Avoid
Single-Line If Constructs
- Use Select
Case Correctly
- Dont
Use Stop Statements for Debugging
- Conclusion
This
is not a tip, rather it is a requirement for
writing VBA code. If you do not use the Option Explicit
statement at the top of every module in your project, you
are asking for trouble when you debug your application.
By specifying Option Explicit, you tell VBA to force you
to declare every variable used. This makes it easy to
avoid program errors because of misspelled variable
names. For example, the following procedure is in a
module that doesnt have the Option Explicit
statement. Can you find the bug?
Sub ShowBugs ()
Dim intX As Integer
Dim intY As Integer
Dim intZ As Integer
intX = 1
imtY = 2 ' Whoops! Here's a typo…
intZ = 3
Debug.Print intX + intY + intZ
End Sub
When
you run this code, you would expect it to assign the
values of 1, 2, and 3 to the intX, intY, and intZ
variables, and then print the sum of the three which
should be 6. However, this procedure prints the value of
4 because of a spelling error. Instead of setting intY to
2, the code is actually setting imtY (with an
"m") to 2. Because Option Explicit is not
declared, the first reference to the variable named imtY
declares the variable automatically. This means that this
code compiles just fine and doesnt become a problem
until it is actually tested. On the other hand, if Option
Explicit is specified, the above code would generate a
compile error, making it easy to track down the error
before you go any further in your development efforts.
Finally,
consider this: if the bug in the example were simple to
spot, what happens when you have the same bug buried in
hundreds of lines of code? Using Option Explicit is the
single most important thing you can do to write better
VBA code.
You can have Option Explicit
automatically inserted into every module you create from the module
editor, select Tools, Option and check the Require Variable Declarations
box.
There
are several guidelines to keep in mind when you declare
your variables with the Dim statement. First, organize
your Dim statements to keep them all together within a
procedure, or with the declarations section of a module.
By placing all Dim statements at the top of a procedure,
you can get a quick inventory of variables that
belong to a procedure. You an also perform
quality-control on your variables by visually scanning
the entire list at onceyou dont have to hunt
and peck through the entire module.
Next,
keep each Dim statement on its own line. Consider the
following code:
Dim intX As Integer, intY, intZ As Integer
A
quick glance at the code may lead you to believe that
three integer variables are being defined. However, a
closer look reveals that intY is not being declared as an
integer (its actually a variant.) If each of these
variables were declared on a separate line, it would be
quickly apparent that intY was not type cast. By applying
this rule to your code, you also avoid the simple Dim
syndrome. Consider the following code:
Dim a, b, c As String
The
intent of this code was to declare three variables as
strings. But VBA doesnt work this way: only c
is declared as a stringthe others are declared as
variants. VBA requires you to explicitly declare each
variable. Otherwise, it is created as a variant which is
less efficient.
You
should also know that VBA lets you specify types for
constants using this type of syntax:
Const strCustomer As String = "Customer"
To
make life easier for beginning developers, VBA offers the
Variant data type. This data type is very flexible
because it can represent almost any type of data, and VBA
automatically handles all conversions from one type to
another. But these strengths are also weaknesses. Because
the variant type must be capable of holding any type of
data, it adds storage overheadit is always more
efficient to store data using a specific data type than
it is to use a variant. Also, since VBA does automatic
conversions on variants, you are not in controlwhen
conversions happen, you may see unexpected results.
Most
of Microsofts Basic variants offer the quaint (and
archaic) type declaration character. By putting one of
these pre-defined characters at the end of a variable or
function name, you define the variables type or the
functions return value. For example, the following
line of code:
Dim CName$
declares
a variable called CName as a string. The dollar sign ($)
character at the end is a type declaration character, and
Access knows that a dollar sign signifies a string
variable. The problem is that type declaration characters
are old-fashioned. They still exist in VBA today
primarily for backward compatibility. Good VBA code
should avoid type declaration characters because they are
considered obsolete. Also, there are only type
declaration characters for a small subset of the data
types that VBA supports.
You
should also avoid the Def
statements, such as
DefInt A-Z. These constructs are also obsolete, and can
lead to code that is difficult to debug and maintain.
All
VBA variables and procedures are scoped to be
available to different portions of the program. For
example, a variable declared as Public in a standard
Access module can be read or assigned from any procedure
in any module in the database. This is the broadest
possible scope. On the other hand, a variable declared as
Private within a procedure or passed as a procedure
parameter has the narrowest scopeit is only seen by
the procedure. Between these two extremes, you also have
module level scoping. When deciding on the scope of a
variable, use the narrowest scope possible. If the
variable only needs to be used by one procedure, make it
private to that procedure. If the variable must be seen
by many procedures in the same module, make it a module
level variable. Only in rare occasions should you create
Public variables. Code with narrow scoping throughout is
much easier to debug and maintain. They are also much
easier to move or copy.
There
are times you need to convert data from one type to
another. VBA makes this deceptively simple by often
performing automatic data type conversions for you. But,
this is not necessarily a good thing. Often, these
automatic conversions can introduce changes in the data
that you do not expect. This is especially true when
converting numbers between integers and doubles. Also,
the semantics and behavior of automatic data type
conversion is often semi-documented or under-documented,
and can change between versions of a language. For these
reasons, you should use explicit VBA conversion functions
in your code such as CInt, CDbl, CStr, etc. Relying on
VBA to do automatic conversions can lead to bugs that are
very hard to find.
One of
the more important tips for variables is to explicitly
declare a data type. The same rule holds true for
functions. When you create a procedure as a function, you
are implicitly telling VBA that the procedure returns a
value. If you do not explicitly declare the return type
of the function with the As keyword, VBA always
casts the functions return value into a variant. As
with variables, this can lead to subtle conversion and
logic bugs in your code.
It
also prevents the code compilation from detecting
incorrect variable assignments. For instance, if you
assign the function to a variable of type string, this is
a compile error if the function explicitly returns a
number, but not a problem if nothing is specified. Your
application will then crash when that line of code is
encountered. Every function should have its return value
explicitly set.
When
an untrapped error occurs in your VBA application, a
number of interesting and unwanted things can happen.
First, a dialog displays the error message, usually with
OK, Cancel and End buttons. If your applications
user is savvy enough to understand the cryptic message
displayed, he or she is then unceremoniously dumped into
the VBA source code at the line where the error occurred.
Such behavior is hardly the hallmark of a professional
application. To make matters worse, the source code may
be secured, causing an even more cryptic error message to
appear. Because of this, error handling is a crucial part
of your application development efforts.
Every
procedure, no matter how small or insignificant, should
have some type of error handling. At the most rudimentary
level, there should be an On Error Goto statement that
points VBA to a label within the procedure. This label
should contain code that, at a minimum, displays a
meaningful error message.
You
may also want to consider creating a procedure stack.
A stack is simply a list of procedures, and the order
they are executing in. This list is useful when your
application encounters an unanticipated error. When such
an error occurs, you can inspect the procedure stack, and
see exactly which procedures were executing, and in which
order. A stack is typically implemented with an array.
Initialize the array when your application starts. Then,
at the beginning of each procedure, place the
procedures name as the last element of the array,
moving all other array elements up by one. Just before
the procedure exits, it should remove its name from the
array. Although this approach requires extra coding
effort, it can be well worth it when trying to debug
applications when the application and user are at a
remote site.
Finally,
Access supports the Erl function. This function returns
the line number of the line where the last error
occurred. You can use this function as part of your error
handling routines to report exactly where the error
occurred. This is extremely useful and can often eliminate the need to
get information from the user when you have the error message and exact
line number.
Unfortunately, this powerful technique only works if you've added
line numbers to your code. One of the features in our
Total Visual
CodeTools product does this. It can add line numbers to all your
code (along with error handling to procedures that lack it), so you can
have a more robust application.
For more information on this topic, visit
Pinpointing the
Exact Line Where A Crash Occurs
VBA
provides many constructs for controlling the flow of your
program. Unfortunately, some of these constructs are
vestiges of the early BASIC days when programs were
non-modular linear lists with required line numbers.
These constructs, namely Goto, Gosub and Return, have
little validity in modern VBA programs. They often lead
to messy jumps and difficult to understand logic. If you
want to write well structured code, you should never use
the Gosub
Return statements, and only use the Goto
statement for error handling.
Equally
important, no procedure should ever have more than one
exit point (Exit Sub or Exit Function commands.) If a
procedure can exit from more than one place, logic
errors, unclosed objects, and mishandled errors can
occur. Finally, consider adopting the convention where
every one of your procedures has a specific error label
and a specific exit label.
Values
that are hard-coded into program code make that program
code difficult to understand and difficult to update.
Consider the following code:
Dim intCost As Integer
Dim intQuantity As Integer
intCost = intQuantity * 1.712
What
does this code do? Thats obvious: it multiplies a
quantity by 1.712 to get the cost. But what is 1.712?
These types of magic numbers offer no information
about what is being accomplished, and make the program
difficult to decipher. If the number needs to be changed,
it may also be very difficult to find. It is much better
to declare a constant for the number.
Next,
consider the following code:
If Err.Number = 2048 Then
MsgBox "Error 2048 occurred", 16, "Acme Application"
Else
MsgBox "Error 2048 didn't occur", 16, "Acmo Application"
Endif
In
this example, hard-coded values cause two problems. The
first is caused by the hard-coded number 16. What does 16
represent? What type of icon is displayed by the MsgBox
function? Second, because "Acme Application" is
hard-coded twice, it is easy to introduce typos, such as
the one shown in the second MsgBox statement. Finally, by
hard-coding literal strings throughout your program, you
make it very hard to easily make global changes.
You
can avoid these problems by replacing hard-coded values
with centralized constants or variables, or by placing
values in a table. When your application starts, it can
read all text values and magic numbers from the table
into memory, making them readily available to your
program. When you remove all hard-coded values, you
reduce the potential for errors, and make the program
more maintainable. This last point can be crucial if you
need to face the issue of internationalization. When you
need to translate your application to another language,
you will really appreciate code that has all literals and
text stored in a central location.
Although
the relative merits of different naming conventions can
cause heated arguments among developers, the use of
naming conventions is generally accepted as a good
programming practice. Naming conventions add information
to variable, object, and procedure names, typically using
some type of prefix or suffix notation.
This type of notation requires you to add several
characters to the beginning or end of a variable name to
identify its type. For example, each string variable you
create would be prefixed with the characters
"str", identifying those variables as strings.
There are many different naming conventions for VBA,
split mainly between Visual Basic and Access developers.
Beyond code, people also use naming conventions for
Access objects (e.g. tables begin with "tbl",
queries "qry", forms "frm", command
buttons "cmd", etc.).
The
naming convention you choose is not as important as your
commitment to use the convention consistently and
constantly. Naming conventions dont work well
unless you use them throughout your code, so pick a
convention and stick to it.
It may
seem obvious that as a developer you should choose
variable and procedure names that convey that
objects purpose. But often, in the heat of
programming, it is all to easy to resort to cryptic
one-letter names like x and y. For
readability, you should avoid such a temptation.
Generally, one letter variable names are reserved for
throwaway variables such as loop counters; beyond that,
you should avoid informationless names.
Unfortunately,
object naming is often at odds with the goal of making
code compactfor documentation purposes, you want
names that are long enough to adequately describe the
variable or procedure. However, you dont want names
that are so long that they make your code unreadable or
difficult to type. Somewhere between the two extremes
lies a happy medium: use names that are long enough to
convey their purpose, but not so long that code becomes
unmanageable.
If you
have ever tried to decipher another developers code
(or your old code), regardless of language, you are no
doubt aware of the value of proper commenting. Many
times, the purpose of a given piece of code is not
readily apparent by reading the code itself. Comments go
a long way to providing the information that makes the
process of understanding, enhancing, and debugging code
much easier.
There
are various levels of commenting in VBA coding. These
levels closely follow the levels of scoping:
Application
(global) comments
Comments
at this level explain the flow of the entire application
and cover how different components and modules interact.
Typically, these comments are kept outside of the
applications project database, but can be included
in a global module as comment text.
Module
level comments
Module
level comments explain the purpose and contents of a
module. Information typically includes a list of the
procedures in the module, their purpose and relationship,
and revision comments for the module as a whole.
Procedure
level comments
Comments
for each procedure typically include a brief description
of what the procedure does, a definition of its
parameters and return value, revision information, and
any special situations it requires.
Line
level comments
Comments
on or above a specific line of code explain the purpose
of a variable, or a particular sequence of operations.
Typically, it is best to include line level comments on
the line immediately preceding the line of code. Comments
on the same line as the code itself can be difficult to
maintain and make code harder to read.
Control
structures should be indented to improve readability and
reduce programming errors. Paired structures such as
If..End If, Do..Loop, For..Next, With..End With,
While..Wend, Select Case .. End Select, Type..End Type,
etc. should all use indenting to clearly show where they
begin and end. Also, you should choose a tab width
setting and stick with itthere is nothing worse
than trying to work with an application that has several
different tab width settings.
Fixing indentations can be a tedious task. One of the features of our
Total Visual CodeTools product is the automated cleanup of your
indentations. Visit Sample VBA/VB6 Code
Formatting and Standardization for more information.
By default, Access uses
four spaces, but if your code has lots of nesting, two
spaces may work better for you. This can be set from the VBA IDE under
the Tools, Options menu selection and checking the Auto Indent box.
Indentation
can also aid the visual grouping of related operations,
not just control structures. For example, consider using
indentation to group AddNew
Update operations,
Set..Close, and BeginTrans
CommitTrans operations.
This makes it easier to see when the code enters and
exits these "paired" operations.
Placing
an If condition and its action on the same line leads to
code that is difficult to read and to maintain. Instead
of this:
If fInitialized Then DoCmd.OpenForm "frmOpen"
do
this:
If fInitialized Then
DoCmd.OpenForm "frmOpen"
End If
The Select Case construct makes it easy for you program to branch
based on multiple possible values for a variable or expression. There
are two things to be aware of when using Select Case. The first is a
matter of style. Avoid putting an action on the same line as a Case
statement. The following code samples illustrate this.
Bad Style which is difficult
to read and maintain:
Select Case intType
Case acTable : strType = "Table"
Case acQuery: strType = "Query"
End Select
A Better Way which is easier to read and expand in the future:
Select Case intType
Case acTable
strType = "Table"
Case acQuery
strType = "Query"
End Select
In the
first example, it is difficult to quickly see which is
the condition and which is the action. Also, when you
come back later to add code to the action clause, you end
up having to format it correctly anywaythe multiple
actions won't fit on a single line anymore! Save yourself
future problems by formatting Select Case blocks
correctly at the outset.
Here's
another tip that can save hours of frustration as you try
to debug a complex application. Unless you always have a
Case Else clause in your Select Case blocks,
unanticipated values will not be acted upon.
For
example, assume your application allows the user to add
new employee categories. Your application is also has VBA
code that gives each employee a raise at the end of the
year based on their job type. Lets assume that the user
adds "Computer Programmer" as a category and a
few people are hired that year as Computer Programmers.
Finally, this is what your Select Case statement to
handle raises looks like:
Select Case intEmployeeType
Case EmpType_Manager
intRaise =10
Case EmpType_Clerical
intRaise = 5
Case EmpType_Driver
intRaise = 2
End Select
Guess
who isn't getting a raise this year? If you follow the
practice of always adding a Case Else clause, problems
like the one shown above are easier to handle. For
example, the above code could be re-written to prompt the
user for the raise amount in the case of new employee
types:
Select Case intEmployeeType
Case EmpType_Manager
intRaise =10
Case EmpType_Clerical
intRaise = 5
Case EmpType_Driver
intRaise = 2
Case Else
Beep
intRaise = InputBox("Please enter raise amount.")
End Select
At
first blush, the Stop statement appears to be an ideal
candidate for debugging. You insert the Stop keyword into
your code at the required place, run it, and VBA obliges
by halting execution when it gets to that line. The
problem is that you may not always remember to remove all
Stop statements before your code enters production use.
There is nothing more embarrassing than seeing your code
open up at a Stop statement while a user is running your
applicationall because you forgot to remove the one
innocuous statement. A better alternative is to use
breakpoints. These perform the same function as the Stop
statement, but are not saved with the code. When you exit
Access, the breakpoints are discarded, ensuring that your
code will not stop unexpectedly.
Writing
robust, maintainable bug-free programming code is a
difficult task. Computer languages are inherently
difficult to use, and make it much to easy for bugs to
appear. However, with a disciplined approach and a good
set of guidelines, your efforts will be rewarded with
programs that work, and work right.
Copyright © 1998, FMS
Inc. All rights reserved. This information may not be
republished, reprinted or retransmitted in any form
without the express written permission of FMS Inc. The
information provided in this document is provided
"as is" without warranty of any kind.

Back to Main Technical
Papers Page
|