


Chapter 13
Modular Programming
This lesson covers the theory of good, structured programming techniques. By breaking
your application into several procedures, you'll streamline your coding efforts,
write more accurate code, and speed subsequent maintenance. Before you can successfully
write well-structured code, you'll have to master argument passing. This lesson examines
Visual Basic's two argument-passing methods and describes when and why you would
choose one over the other.
The highlights of this hour include
- What benefits structured programming offers
- Why short, numerous procedures beat long procedures
- How to write your own functions and subroutines
- When to use functions
- How to code argument lists
- Why VB uses two argument-passing methods
- How to protect passed arguments
Structured Programming
You already know the best way to structure programs because you can use Microsoft's
Visual Basic design as a guide. The small event procedures you've seen and coded
are perfect examples of the correct way to code. Don't write long routines that do
everything; instead, write small code procedures that each perform only one task,
such as respond to a user's keystroke. If the keystroke is to trigger a bunch of
things, keep the event procedure small and call other small procedures that do the
detailed work.
New Term: Structured programming is
a programming method you use to break long programs into numerous small procedures,
putting off the details as long as possible.
For example, suppose that you need to perform the following tasks when the user
clicks a Reconcile command button in a checkguide application:
- 1. Display checks next to cleared items.
2. Total the cleared items.
3. Total the uncleared items.
4. Recommend an action if the manual checkguide balance and the checkguide computer
file's balance do not match.
5. Print a reconciliation report.
Such a detailed response to a single command button click would take many screens
of code. Nevertheless, the Click() event procedure does not have to be many
screens. Instead, you could insert a series of procedure calls that do the detailed
work and keep the Click() procedure small like this:
Private Sub cmdReconcile_Click ()
Call ClearItems ()
Call UnClearItems ()
If ChkBkIsBalanced () Then
Call OutBalanceAction ()
End If
Call ReconcilePrint ()
End Sub
TIP: You are now learning
about a topic called structured programming. In structured programming you delay
coding details for as long as you can. Keep subdividing your code procedures so they
simply control procedures that call more detailed procedures until you finally reach
the point where a task cannot be further subdivided.
All of this event procedure's called procedures should themselves be as small
as possible and only perform a single task, or a series of calls to other procedures.
All of your code becomes a structured, manageable set of routines that each perform
a single task or that control other tasks.
Not only does structured programming make writing code easier, it makes managing
code really simple. If your application contains a bug, you can more easily locate
the bug because you follow the thread of procedures until you get to the routine
that controls the logic with the bug. If your unclear balance is incorrect, you can
go directly to the procedure that computes that balance and then locate the problem
without affecting lots of other code around that routine.
New Term: The called procedure is the
procedure called by another procedure.
New Term: The calling procedure is
the procedure that triggers another's execution.
Calling Procedures
and Returning from Them
The previous section discusses calling procedures. You have learned about the
Call keyword, but you've not been exposed to Call before now. That
is, you've not been directly exposed to Call even though you have performed
a similar action by using the built-in Val() and Format() functions.
When one procedure contains a Call statement, the Call statement
puts the current procedure on hold and executes the called procedure. Here is one
of the formats of the Call statement:
Call Procedure
NOTE: The Call
keyword is sometimes optional, as you'll see later in this lesson.
Therefore, when one procedure's execution reaches its Call statement,
that procedure is put on hold and execution begins at the called Procedure. Once
the called procedure ends (whether it ends with the End Sub statement or
an Exit Sub statement or by other means), the called procedure returns control
to the calling procedure. The same thing happens when you call the built-in functions
because a built-in function is a special kind of procedure: Your code temporarily
stops, and the built-in function's code takes over and uses the argument and finally
returns a value as well as control back to your code.
You've seen event procedures and you've executed the built-in function procedures,
and Visual Basic supports two other kinds of procedures:
- Standard subroutine procedures
- Standard function procedures that you write
A standard subroutine or function procedure does not respond to an event. A standard
procedure only executes when called from elsewhere in the program.
WARNING: If a procedure
is defined with the Private keyword, then only procedures elsewhere within
that module can call that procedure. If a procedure is defined with the Public
keyword, all procedures in the project can call the procedure.
Standard procedures, whether they are subroutines or functions, can reside either
inside a form module (following the event procedures) or inside an external module
file you add to your project. Figure 13.1 illustrates the difference between subroutines
and functions. The calling code calls both and they both do work. The subroutine
does not return a value to the calling procedure. The function does return a value
to the calling procedure, and the calling procedure must do something with that value
such as assign the value to a variable or control. By the way, you'll understand
all that's happening in Figure 13.1 before this lesson is over, so if some of it
confuses you right now, don't be alarmed.
Why Code External
Modules?
Generally, programmers put general-purpose Public procedures in their
external modules (modules that are not form modules). These general-purpose subroutines
and functions perform work such as calculations and printed output that you may want
to repeat in several different applications. For example, if you want to incorporate
Visual Basic code that prints your letterhead in two or more applications, you can
write the code once, store the code in a standard module, and then add that module
to whatever application needs the letterhead printed. The application's regular form
module code might call the external module's letterhead routine when ready for the
printed letterhead, such as before the body of a specific report prints. To add an
external module to a project, simply right-click over the Project Explorer window
and select Add Module. The extra module appears in the Explorer window and in the
Code window. You then can switch between modules by double-clicking the module name
in the Explorer window. The Sub keyword indicates that you're coding a subroutine
and Function indicates that you're writing a function. Of course, you can
put standard subroutines and functions inside form modules and you should do that
if your event procedures get too long. The standard procedures serve to break down
the longer problem into more manageable structured routines, as described earlier
in this lesson.
Figure
13.1. Both subroutines and functions do
work, but only functions return values.
As Figure 13.1 illustrates, when you want to write a procedure that performs a task
but does not need to return a value, write a subroutine procedure. If you need to
write a procedure that performs a task and returns a value, such as a calculated
result, write a function procedure. You can pass arguments to either kind of procedure.
New Term: A standard function procedure
is a standalone non-event procedure that does work when called by another procedure
and returns a single value to that called procedure.
New Term: A standard subroutine procedure
is a standalone non-event procedure that does work when called by another procedure.
Coding Subroutines
You'll find uses for subroutines as you begin writing larger applications. For
example, suppose you were writing a company sales status program. You might need
a specialized routine that calculates a cost of sales value and displays that value
in a label. By putting that code in a subroutine procedure, you help separate the
task from other tasks and make the application more manageable. In addition, if several
procedures in the application need the calculation, you can call the procedure from
every place that needs it instead of repeating the same code in every place.
To create a subroutine procedure, perform these steps:
- 1. Make up an appropriate name for the procedure using the same naming
rules as you use for variables. Give the procedure a meaningful name such as CostOfSales.
2. Determine whether you want to put the procedure in the form module or in a
separate external module. If you think you'll use the code in other applications,
add a new module to your Project Explorer window, but if the code goes with this
application only, you can add the code to the current form module.
3. Open the Code window and scroll to the bottom. On a blank line below the last
line type Private Sub CostOfSales(). (If you fail to type the parentheses,
Visual Basic adds them for you because all procedure names terminate with the parentheses
to hold possible arguments.) As soon as you press Enter, Visual Basic adds the end
of the procedure, as shown in Figure 13.2's Code window.
Figure
13.2. You must fill in the procedure's body.
TIP: Instead of locating
the end of the module and typing the first line, you could also select Tools | Add
Procedure to open Figure 13.3's dialog box and set up a new subroutine (or function)
procedure.
Figure
13.3. You can insert new procedures from
this Add Procedure dialog box.
Once Visual Basic creates the place for the procedure, you can add the body of the
code. For example, Listing 13.1 shows how you might code a cost of sales subroutine
procedure. The procedure's job is to calculate the cost of sales from text box values
and assign the cost to a label named lblCost.
WARNING: If you
put code such as Listing 13.1 in an external module, you must precede all control
names with the form name that contains those controls. Therefore, precede the text
boxes and labels with the form name that contains those text boxes and labels (for
example, frmSales.txtTotalInv.Text and frmSales.lblCost.Caption).
Listing 13.1. A
cost of sales subroutine.
Private Sub CostOfSales()
` Computes a cose of sales and
` displays that code in a label
Dim curGrossSales As Currency
Dim curCostSales As Currency
Dim sngOverHead As Single
Dim sngInventoryFctr As Single
Dim sngPilferFctr As Single
` Store initial variable values from controls
curGrossSales = txtGross.Text
sngInventoryFctr = txtTotalInv.Text * 0.38
sngPilferFctr = txtPilfer.Text
sngOverHead = 0.21 ` Fixed overhead percentage
curCostSales = curGrossSales - (sngInventoryFctr * curGrossSales)
curCostSales = curCostSales - (sngPilferFctr * curGrossSales)
curCostSales = curCostSales - (sngOverHead * curGrossSales)
lblCost.Caption = Format(curCostSales, "Currency")
End Sub
NOTE: Use default
property values for the text boxes and labels if you want to shorten your code somewhat.
Coding just txtTotalInv accomplishes the same purpose as coding txtTotalInv.Text
because Text is the default property for all text boxes. Caption
is the default property for labels.
To call this procedure, another procedure (such as a Click() event procedure
or another standard procedure) can issue either of these statements:
Call CostOfSales() ` Calls the CostOfSales() subroutine
CostOfSales ` Calls the CostOfSales() subroutine
If the subroutine uses no arguments, you don't need to use Call and the
parentheses to trigger the subroutine's execution. If CostOfSales() did
use one or more arguments, you would not need Call, but you could leave
off the Call keyword.
Coding Functions
You can write your own general-purpose function procedures that are not tied to
specific events. You can call these functions from any Visual Basic application just
as you can subroutine procedures. Function procedures work just like subroutine procedures
in every way; you call them from elsewhere in the code. Unlike subroutine procedures,
however, a function procedure always returns a value.
If you run across a needed calculation and Visual Basic has no built-in function
equivalent, you can write your own function that returns that calculated value. When
you call the function, you must do something with the returned value. You cannot
put a function call on a line by itself as you can with a subroutine. If CalcTax()
is a function, you cannot call the function like this:
CalcTax () ` Problem!
The CalcTax() function will return a value and you must do something
with that value. Therefore, you'll usually assign the return value like this:
lblAmt.Caption = CalcTax() ` Okay
You can also use the function call inside an expression, like this:
curAmount = Estimate * .2 + CalcTax() * .14
TIP: You should code
as though the function call becomes its return value. In other words, when CalcTax()
returns from doing its job, the return value temporarily replaces the function call
inside the expression.
The functions that you write aren't quite as built-in as Visual Basic's built-in
functions, but they behave the same way. Your functions never become part of VB's
repertoire, but you can put them in any module that needs to access them. Over time,
you will write many general-purpose function and subroutine procedures and you might
want to keep a module library of common routines that you'll use throughout different
applications. To use one of the procedures that you write, you can add that procedure's
module to whatever application needs the procedure.
You will write new function procedures the same way you write new subroutine procedures
(with Tools | Add Procedure or by typing the first function procedure's line at the
end of the module). Use the Function keyword in place of Sub. The
following statements would code the beginning and ending statements from a CalcTax()
function:
Public Function CalcTax () As Single
End Function
You'll notice something extra on that function's opening statement: As Single.
In addition to using the Function keyword, you must also specify the function's
return value data type in the function's opening declaration line. Therefore, this
CalcTax() function returns a single-precision data type.
Listing 13.2 contains a function that computes the postage for a letter or package
using the following rules:
- 1. The post office charges 32 cents for 8 ounces or less.
2. Add 15 cents for each 4 ounces above the first 8.
3. The weight cannot exceed 24 ounces.
The function's code assumes that the letter or package weight appears in a text
box control named txtWeight.Text. In addition, the weight must appear as
ounces. Therefore, any application that uses this function must make sure these conditions
are met before calling the function.
NOTE: Listing 13.2's
function procedure uses no arguments. You'll learn how to code arguments in the next
section.
Listing 13.2. Calculating
postage with a function procedure.
Public Function Postage() As Currency
` Calculate postage based on the
` weight of a letter or package
Dim curPostHold As Currency
Dim intWeight As Integer
Dim intPress As Integer ` MsgBox() return
` Grab the weight from the text box
` and convert to number for comparison
intWeight = Val(txtWeight.Text)
Select Case intWeight
Case Is <= 8: curPostHold = 0.32
Case Is <= 12: curPostHold = 0.47
Case Is <= 16: curPostHold = 0.62
Case Is <= 20: curPostHold = 0.77
Case Is <= 24: curPostHold = 0.92
Case Is >= 24:
intPress = MsgBox("Weight is too heavy", _
vbExclamation, "Error")
curPostHold = 0#
End Select
Postage = curPostHold ` Return the value
End Function
Listing 13.2 demonstrates the way you return the value from a function. There is
no variable declared named Postage, yet the second-to-last line assigns
a value to Postage. Postage is the name of the function, not a
variable! Inside a function procedure, when you assign a value to the function's
name, the function uses that value as the return value. This function does not actually
end until the End Function statement is reached, but the return value is
set right before the terminating statement.
NOTE: If you ever
need to terminate a subroutine or function from somewhere in the body of the routine
instead of at its normal termination point, use the Exit Sub or Exit
Function statement. Be sure to set a return value of some kind to the function
name before terminating a function because the function requires a return value.
Coding Arguments
Variables that are local to a procedure can only be used inside that procedure.
Variables declared inside a module's general section are global to the module
and available throughout the entire module. Variables declared with Public
instead of Dim inside the general section are global to the entire
project.
You've seen throughout the first part of this guide that you should avoid global
variables as much as possible and use only local variables. If, however, you only
use local variables but you write lots of small procedures (as you should), how can
the procedures share data? If all the data is local, then a called procedure has
no access to the calling procedure's data. As you probably suspect, you'll share
data through argument lists. When one procedure must call another procedure, and
the called procedure needs information from the calling procedure, the calling procedure
can send that information inside the argument list.
Suppose one procedure calculates a value and a second procedure must use that
value in a different calculation before displaying a result on the form. You need
to know how to pass local data from the procedure that defines the local variable
to other procedures that need to work with that value.
When you call a built-in function, you pass one or more arguments to the function
so that the function's internal code has data to work with. When you call your own
subroutine and function procedures, you also can pass arguments to them. The arguments
are nothing more than the passing procedure's local variables that the receiving
procedure needs to work with.
Once you pass data, that data is still local to the original passing procedure,
but the receiving procedure has the opportunity to work with those values for the
time of the procedure execution. Depending on how you pass the arguments, the receiving
procedure might even be able to change those values so that when the passing procedure
regains control, its local variables have been modified by the called procedure.
NOTE: The passed argument
name (or names) does not have to be the same as used in the receiving procedure.
Therefore, you might call a subroutine with Call CalcIt(X) and the subroutine
begins with this declaration line: Public Sub CalcIt(Y As Int). Although
in this case both X and Y refer to the same value, the receiving
subroutine procedure uses a different name from the passing procedure. The only argument
list requirements are that the calling and receiving argument lists must match in
number of arguments and they must match in data type order.
You must declare the receiving argument list's data types for each argument. If
you must pass and receive more than one argument, separate the passed arguments and
the received arguments (along with their declared data types) with commas. The following
statement passes the three values to a subroutine:
Call RecProc(I, J, K)
The following statement declares the RecProc() procedure:
Public Sub RecProc (I As Integer, J As Integer, K As Single)
The calling procedure already knows the data types of I, J,
and K, but those values are unknown to RecProc(). Therefore, you'll
have to code the data type of each received argument so that the receiving function
knows the data type of each sent argument.
If a subroutine or function procedure is to receive arrays, don't indicate the
array subscripts inside the argument list. The following Sub statement defines
a general-purpose subroutine procedure that accepts four arrays as arguments:
Public Sub WriteData (GNames() As String, CBalc() As Currency,
ÂCDate() As Variant, CRegion() As Integer)
The built-in UBound() function returns the highest subscript that's defined
for any given array. The following statement, which might appear inside the WriteData()
subroutine, stores the highest possible subscript for the CNames() array,
so the subroutine won't attempt to access an array subscript outside the defined
limit:
intHighSub = UBound(CNames)
Remember that Call is funny about its argument parentheses. If you use
Call, you must also enclose the arguments in parentheses. You may omit the
Call keyword, but if you do, omit the parentheses as well. Here is a Call
statement equivalent to that shown earlier with parentheses:
RecProc I, J, K ` No Call, no parens!
Receiving by Reference
and by Value
Visual Basic lets you pass arguments two ways: by reference and by value. The
way you use them determines whether the receiving procedure can change the arguments
so that those changes remain in effect after the calling procedure regains control.
If you pass and receive by reference (the default method), the calling procedure's
passed local variables may be changed in the receiving procedure. If you pass and
receive by value, the calling procedure can access and change its received arguments,
but those changes don't retain their effects in the calling procedure.
NOTE: Passing by reference
is sometimes called passing by address. In some languages, by address and by reference
mean two different things, but not in Visual Basic.
When passing by reference, subroutines and functions can always use their received
values and also change those arguments. If a receiving procedure changes one of its
arguments, the corresponding variable in the calling procedure is also changed. Therefore,
when the calling procedure regains control, the value (or values) that the calling
procedure sent as an argument to the called subroutine may be different from the
situation before the call.
New Term: By reference is a way in
which you pass values and allow the called procedure to change those values. Also
called by address.
New Term: By value is a way in which
you pass values and protect the calling procedure's passed data so that the called
procedure cannot change the data.
Arguments are passed by reference, meaning that the passed arguments can be changed
by their receiving procedure. If you want to keep the receiving procedure from being
able to change the calling procedure's arguments, you must pass the arguments by
value. To pass by value, precede any and all receiving argument lists with the ByVal
keyword, or enclose the passed arguments in parentheses.
NOTE: If you want
to be clear, use the ByRef keyword. But passing by reference is the default
method if you don't specify ByRef.
It's generally safer to receive arguments by value because the calling procedure
can safely assume that its passed values won't be changed by the receiving procedure.
Nevertheless, there may be times when you want the receiving procedure to permanently
change values passed to it, and you'll need to receive those arguments by reference.
Listing 13.3 shows two subroutine procedures. One, named Changes(), receives
arguments by address. The second procedure, NoChanges() receives its arguments
by value. Even though both procedures multiply their arguments by two, those changes
affect the calling procedure's variables only when Changes() is called but
not when NoChanges() is called.
Listing 13.3. Some
procedures can change the sending procedures arguments.
Sub Changes (N As Integer, S As Single)
` Receives arguments by reference
N = N * 2 ` Double both
S = S * 2 ` arguments
` When the calling routine regains control,
` its two local variables will now be twice
` as much as they were before calling this.
End Sub
Sub NoChanges (ByVal N As Integer, ByVal S As Single)
` Receives arguments by value
N = N * 2 ` Double both
S = S * 2 ` arguments
` When the calling routine regains control,
` its two local variables will not be
` changed from their original values
End Sub
As you can see, Changes() receives its arguments by reference. (Remember
that the default passing method is by reference, even if you omit ByRef.)
Therefore, when the procedure doubles the arguments, the calling procedure's argument
variables change as well.
In NoChanges(), the procedure receives its arguments by value. Therefore,
nothing NoChanges() does can change those values in the calling procedure.
Summary
In this lesson you have learned how to write programs that are properly structured
so that you can more easily and quickly write and debug the code. By coding small
and numerous modules, and by putting off details until you're ready to code a procedure
that performs a single task (although that task may take a few statements), you'll
write code that you can easily debug and modify later.
Once you break a program into several procedures, however, you must be careful
to pass arguments to the procedures that need them. The way you pass arguments determines
how the passing procedure's argument values change. If you pass by reference, the
passing procedure's values are protected and always left unchanged, no matter what
the called procedure does to them.
Now that you've learned how to write your own procedures, you're ready for Hour
14, "Built-in Functions Save Time," which describes many of VB's built-in
functions that you can use in your own programs.
Q&A
- Q I've always coded long procedures and my programs work, so why should I
write structured code now?
A If your way works well, the structured way would be working even better. When
you test your applications, you must wade through lots of code, searching for problem
areas. When you test structured applications, however, you can usually narrow the
bug down to one or two small procedures. Making a change to correct the bug rarely
affects other procedures, but when your code is in a few long procedures that do
lots of work, a change could adversely affect surrounding code.
Q If I'm careful, what does it matter how I receive arguments?
A The method you use to pass and receive arguments, either by reference or by
value, does not just protect data. Sometimes you want a called procedure to change
the calling procedure's argument values. A function procedure can only return a single
value, but if you want a function procedure to modify several values, pass those
values by reference and then make the function procedure (or even the subroutine
procedure) modify each of those values. When the calling procedure regains control,
the passed arguments will hold values changed by the called procedure.
Workshop
The quiz questions and exercises are provided for your further understanding.
See Appendix C, "Answers," for answers.
Quiz
- 1. What are two reasons for writing structured programs?
2. True or false: Structured code is useful for getting to code details as
fast as possible.
3. True or false: You can write your own functions.
4. What is wrong with the following subroutine declaration?
Public Subroutine DoItSub ()
- 5. When is the Call keyword optional in subroutine calling?
6. The following code appears in a form module's general section.
Is X a local, module-global, or project-global variable? What about Y?
Would your answers be different if this appeared in an external module as opposed
to a form module?
Dim X As Integer
Public Y As Integer
- 7. What is wrong with the following function declaration?
Public Function DoCalc(intAge As Integer, strCoNames(45) As String)
- 8. Why does the called procedure need to know the data types for passed
values?
9. How does one procedure get local data from a calling procedure?
10. Which keyword is optional: ByRef or ByVal?
Exercises
- 1. Write a general-purpose standard function procedure that accepts a
numeric integer argument and returns that argument multiplied by 10.
2. Write a standard subroutine procedure that accepts three single-precision
arguments and displays those three values in labels named lblSng1, lblSng2,
and lblSng3.

