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
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 checkbook application:
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.
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:
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:
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.
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.
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:
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.
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).
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.
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:
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.
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.
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 book 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!
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.
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.
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.
The quiz questions and exercises are provided for your further understanding. See Appendix C, "Answers," for answers.
Public Subroutine DoItSub ()
Dim X As Integer Public Y As Integer
Public Function DoCalc(intAge As Integer, strCoNames(45) As String)