Hour 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

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 checkbook 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 checkbook balance and the checkbook 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:

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 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!

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.