|[ Team LiB ]|
Recipe 7.2 Create a Global Procedure Stack
When you're writing an application, you often need to know the name of the current procedure from within your code. For example, if an error occurs, you'd like to be able to have a generic function handle the error and display the name of the procedure in which the error occurred (and all the procedures that have been called on the way to get there). VBA doesn't include a way to retrieve this information. How can you accomplish this?
By maintaining a list of active procedures, adding the current name to the list on the way into the procedure and removing it on the way out, you can always keep track of the current procedure and the procedure calls that got you there. There are many other uses for this functionality (see the next solution, for example), but one simple use is to retrieve the name of the current procedure in a global error-handling procedure.
The kind of data structure you'll need for maintaining your list is called a stack. As you enter a new procedure, you "push" its name onto the top of the stack. When you leave the procedure, you "pop" the name off the stack. Figure 7-2 shows a graphical representation of a procedure stack in action. The arrows indicate the direction in which the stack grows and shrinks as you add and remove items.
? A( )
to execute the function named A. Figure 7-2 shows A and the procedures it calls. At each step, the current procedure pushes its name onto the procedure stack and then calls some other procedure. Once the calling procedure regains control, it pops its name off of the stack. In addition, each procedure prints the name of the current procedure (using the acbCurrentProc function, discussed later in this solution) to the Immediate window. Once all execution has finished, you should see in the Immediate window output like that shown in Figure 7-3.
Follow these steps to incorporate this functionality into your own applications:
The module you imported from 07-02.MDB, basStack, includes code for maintaining the procedure stack and a module-local variable that is the stack itself. There are just six entry points (nonprivate procedures) in the module. Table 7-1 lists those procedures. Since all the code for the stack is encapsulated in that one module, you never really have to know how it all works. However, it's quite simple.
basStack includes two module-level variables: mastrStack, the array of strings that is the stack itself; and mintStackTop, an integer that holds the array slot into which the next stack item will be placed. When you begin your work with the stack, mintStackTop must be 0, so the first item will go in the slot numbered 0. The acbInitStack procedure does nothing other than initialize mintStackTop:
Public Sub acbInitStack( ) ' Resets the stack top to 0. mintStackTop = 0 End Sub
You can add an item to the stack at any time by calling acbPushStack. Pass to this subroutine the item you want pushed. To push the item, the code places the item in the array at the location stored in mintStackTop and then increments the value of mintStackTop. Its code is:
Public Sub acbPushStack(strToPush As String) ' Push a string onto the call stack. ' If the stack is full, display an error. ' Otherwise, add the new item to the call stack. ' Handle the error case first. If mintStackTop > acbcMaxStack Then MsgBox acbcMsgStackOverflow Else ' Store away the string. mastrStack(mintStackTop) = strToPush ' Set mintStackTop to point to the NEXT ' item to be filled. mintStackTop = mintStackTop + 1 End If End Sub
The only problem that might occur is that the stack might be full. The constant acbcMaxStack is originally set to 20, which should be enough levels. (Remember that mintStackTop goes up one only when a procedure calls another procedure. If you have 20 levels of procedure calling, you might consider rethinking your application, instead of worrying about procedure stacks!) If the stack is full, acbPushStack will pop up an alert and will not add the item to the stack.
When leaving a procedure, you'll want to remove an item from the stack. To do so, call the acbPopStack procedure:
Public Sub acbPopStack( ) ' Pop a string from the call stack. ' If the stack is empty, display an error. ' Otherwise, set the current item to be the ' next one to be filled in. If you're logging, ' send the information out to the log file. ' Handle the error case first. If mintStackTop = 0 Then MsgBox acbcMsgStackUnderflow Else ' Because you're removing an item, not adding one, ' set the stack top back to the previous row. Next time ' you add an item, it'll go right here. mintStackTop = mintStackTop - 1 End If End Sub
Just as in acbPushStack, this code first checks to make sure that the stack integrity hasn't been violated; you can't remove an item from the stack if there's nothing to remove! If you try, acbPopStack will pop up an alert and exit. If the stack is intact, the procedure will decrement the value of mintStackTop. Decrementing that value sets up the next call to acbPushStack so that it will place the new value where the old one used to be.
To retrieve the value at the top of the stack without pushing or popping anything, call the acbCurrentProc function:
Public Function acbCurrentProc( ) As String ' Since mintStackTop always points to the next item to ' be filled in, retrieve the item from mintStackTop - 1. If mintStackTop > 0 Then acbCurrentProc = mastrStack(mintStackTop - 1) Else acbCurrentProc = "" End If End Function
This function retrieves the value most recently placed on the stack (at the location one less than mintStackTop, because mintStackTop always points to the next location to be filled). You can't look at mastrStack yourself, because it's local to basStack—and that's the way it ought to be. Since the details of how the stack works are kept private, you can replace basStack, using a different architecture for the stack data structure, and the rest of your code won't have to change at all.
To retrieve more information about what's in the stack, you can call acbGetStackItems, to find out how many items there are in the stack, and acbGetStack, which retrieves a specific item from the stack. For example, write code like this to dump out the entire stack (see subroutine D, which does just this, in the basTestStack module):
Debug.Print "Stack items currently:" For intI = 0 To acbGetStackItems( ) - 1 Debug.Print , acbGetStack(intI) Next intI
The acbGetStackItems function is simple: it returns the value of mintStackTop, because that value always contains the number of items in the stack:
Public Function acbGetStackItems( ) As Integer ' Retrieve the number of items in the stack. acbGetStackItems = mintStackTop End Function
The acbGetStack function is a little more complex. It accepts an item number (requesting item 0 returns the item at the top of the stack) and calculates the position of the item to retrieve. Its source code is:
Public Function acbGetStack(mintItem As Integer) As String ' Retrieve the item that's mintItems from the top of the ' stack. That is, ' ? acbGetStack(0) ' would return the same value as acbCurrentProc. ' ? acbGetStack(3) would return the third value from the top. If mintStackTop >= mintItem Then acbGetStack = mastrStack(mintStackTop - mintItem - 1) Else acbGetStack = "" End If End Function
For the procedure stack to work, you have to place calls to acbPushStack and acbPopStack on entry and exit from every procedure call. Good coding practice supports the concept of only one exit point from each procedure, but even the best programmer sometimes breaks this rule. To use the call stack, however, you must catch every exit point with a call to acbPopStack. Keep this in mind as you retrofit old code to use this mechanism and when you devise new code to use it. You can always code for a single exit point, and you will find code maintenance much easier if you do.
|[ Team LiB ]|