Dealing with Complexity

Building a mechanism like the one above is a good way to get a feel for the architectural design of your GUI. However, it's tempting to channel design resources into such details as how many forms you're going to use and what fields you need on each form and to let seemingly peripheral issues such as navigation and modality take a back seat. You can get away with this for smaller projects because it's possible to have an intuitive grasp of the interactions among a small number of forms. In general, however, ignoring questions such as how each form is invoked and which forms can be on the screen at the same time can lead to disaster.

By allowing multiple forms to be active at the same time, you dramatically increase the complexity of the underlying code because manipulating data on one form can affect other forms that are also being displayed. For even a modest-size application, the view ahead is daunting, since you have to examine every possible combination of forms that can be active at the same time and then consider the effects of all possible inputs for each combination. Any modes you've designed will help to limit form interactions, but doing the analysis ad hoc invites unplanned side effects—inevitably, you'll fail to plan for some situations.

Figure 13-3 shows an application that manages a cascade of forms that lead the user down through successive layers of data. If you use modeless forms, editing the data on form A invalidates the data on forms B and C. You also need to consider what happens if the user closes form A before closing forms B and C. Clearly, you need to decide whether these actions have any useful meaning in the application; if they don't, you can simply prevent them. If they do, you want to know up front because they can affect the implementation. (You don't want the forms to share data, for example.)

You might be feeling skeptical about the complexities of a three-form application. Some simple arithmetic can be illuminating. There are seven possible ways in which combinations of the modeless forms A, B, and C can be active. (Let's call these combinations "states.") Now let's say the user presses the Cancel button on form A. Let's also assume you've decided never to leave forms B and C up when the user closes form A. It's clear that the event handler for form A's Cancel Click can't simply unload the form—it must look around to see what other forms are up and maybe close those forms too.

Figure 13-3 A cascade of forms—but modal or modeless?

If you add another form to the application, the number of states goes up to 15. Even discarding the states that don't contain form A, you are left with 8 different situations to consider in each Cancel Click event handler. In fact, the number of states (combinations of forms) is 2n -1, where n is the number of forms. This number increases geometrically as you add forms, which means the number of states gets out of hand very quickly. There is a set of events for which you must consider all of these states, and the handlers for such events need to be aware of the environment and adjust their behavior accordingly.

It should be clear by now that you need a formal way to define the interactions between forms. The model we'll use to track form interactions is the finite state machine (FSM). The principles of an FSM, which is essentially an abstract representation of a set of states and events, are described in the following section.

The Art of the State

An FSM is a virtual machine characterized by a set of internal states, a set of external events, and a set of transitions between the states. You might also hear FSMs referred to by the name finite state automata, deterministic finite automata, or simply state machines. FSMs can be used to model an entire application, a small part of it, or both, and they are extremely common in the design of real-time systems, compilers, and communications protocols. FSMs are ideal tools for representing event-driven programs such as GUIs.

States are labels assigned to particular sets of circumstances within the application. Although FSMs are often used to model the GUI part of applications, states are not forms, and events are not necessarily Visual Basic events. You generate a set of predefined events from real-world stimuli and apply them as inputs to the FSM to drive it through transitions into different states. Transitions can have arbitrary lists of actions associated with them, and these actions are executed as you drive the FSM from state to state by repeatedly applying events. An FSM is deterministic because each combination of state and event unambiguously defines the next state to move into.

An FSM can be represented as a state transition diagram or as a pair of tables, one table defining the next state to move into when a particular state/event combination is detected and the other table defining a list of actions to be performed along the way.

Figure 13-4 shows an FSM for a program to strip C comments out of a text stream. (Comments in C are delimited by /* and */.)

Figure 13-4 Comment stripper FSM

The bubbles in Figure 13-4 represent states, and the arrows represent transitions between states. Each transition is labeled with the event that stimulates it and the list of associated actions. One state is designated the start state, which is the initial state when the FSM starts to operate. Here is the FSM in tabular form:

State Table

  State
Event Outside Starting Inside Ending
/ Starting Starting Inside Outside
* Outside Inside Ending Ending
Any other Char Outside Outside Inside Inside

Action Table

  State
Event Outside Starting Inside Ending
/ N/A Print "/ " N/A N/A
* Print char N/A N/A N/A
Any other char Print char Print "/ "    
Print char N/A N/A    

These tables provide the basis for implementing an FSM as a program. An FSM program has the following elements:

  • A static variable to track the current state and a set of constants to represent all available states
  • A table or equivalent program network to look up a state/event pair and decide which state to move into
  • A set of constants to represent FSM events
  • A driver loop that captures real-world events and decodes the state/event pair

Modeling a GUI with an FSM

Figure 13-5 shows a GUI fragment modeled on a real application. This application provides a summary and two different detailed views of a database. The forms are modeless, so the user can edit any form on the screen, potentially invalidating data on either of the other forms. Although the maximum possible number of states is seven, the design of this application permits access to only four combinations of forms: A, A + B, A + C, and A + B + C. The only events we'll consider are the button clicks; there are 11 buttons, so 11 is the number of events that must be accounted for in every state.

Figure 13-5 A deceptively simple-looking application

The application has been designed so that only form A's OK or Apply button commits data to the database. Each form has a buffer in which it holds edits to its own subset of the data, and the contents of these buffers are shuffled around as the OK, Apply, and Cancel buttons are manipulated on forms B and C. Figure 13-6 shows the state transitions for this GUI, and Figure 13-7 is a close-up view of two states, showing the actions the application will take on each transition.

Figure 13-6 FSM for the application shown in Figure 13-5

Close examination of Figures 13-6 and 13-7 reveals some omissions. There are 11 events, but not all states have 11 arrows leaving them. This is partly because not all events can occur in all states. For example, it isn't possible to click form C's Apply button in state 1. But some events, such as the Details button events in states 2 and 3, are omitted because there just isn't enough space for them. Leaving out events like this undermines a basic reason for using an FSM, which is to verify that you've considered all state/event combinations. This is where the tabular form is a much better representation—it's easier to draw, and it clearly shows all state/event combinations. The two notations complement each other: in practice the state diagram is a useful sketch that conveys the feel of the GUI, while the tables provide a basis for implementation.

Figure 13-7 Close-up view of a fragment of Figure 13-6

The End of the Elegance

The finite state machine (FSM) notation is simple and elegant, but you'll run into problems when you try to apply it to real programs. One class of problem, the conditional state transition, is exemplified by the need for validation when you're unloading forms. For example, if you consider form B's OK Click event, you can see that the FSM changes state and does the associated actions unconditionally. If you want to do a form-level validation before committing changes, you'll have a problem. In practice, the solution depends on how far you're prepared to go in carrying the FSM through into the implementation of your program. For smaller applications, it's wise to stop at the design stage and just use the state diagram and tables for guidance when writing the program. For more complex programs, you can carry the FSM right through to the implementation, as you'll see below.

For a pure FSM implementation, you can get around the validation issue by introducing extra states into the machine. Figure 13-8 shows a new state between states 2 and 1 for form B's OK event. The only difference is that this state is transient because the FSM immediately flips out of it into state 1 or state 2. This happens because you queue an event for the new state before you even get there. Validation states are also required for confirmation, such as when a user tries to abandon an edited form without saving changes.

Figure 13-8 Introducing transient states to avoid conditional transitions

Implementing FSMs

If you want to carry an FSM through to the bitter end, you can implement it directly as program code. This requires a leap of faith because the code can often appear long-winded. In spite of this, if you're taking the trouble to implement the FSM, you'll gain much more by sticking rigorously to the mechanism without being tempted to introduce shortcuts, particularly in trying to avoid repetition of code. Recall that we're using an FSM to formalize the design of the GUI, and for a complex GUI the direct translation to code pays dividends by virtually eliminating the need for debugging. By introducing shortcuts, not only do you lose this integrity, but you also make the code harder to read.

Building an FSM with code is a straightforward affair that can be abstracted in a simple conditional statement:

If we're HERE and THIS happens Then      do THAT and GoTo THERE 

The only thing you have to keep track of is the current state, and most of your effort will be concerned with the mechanics of processing events and invoking the action procedures. You can build an FSM in any language that supports conditional statements, so let's start by looking at an implementation that can be adapted to any version of Visual Basic.

For this example, you will implement the C comment stripper described earlier and build it into a simple application using the form shown in Figure 13-9. The application displays the text as you type, minus any C-style comments. You will drive the FSM in real time—that is, the events will be caused directly by your keypresses, and the states and events will be displayed in the other boxes on the form.

Figure 13-9 The comment stripper FSM program

The first thing you need is a state, which can be represented as a simple integer. It doesn't matter what data type you choose for the state, since there is no concept of ordering. The only requirement is that the states be unique. In real life, you'll usually want to define constants for the states and events. In this example, however, you're not going to use event constants because it's convenient to represent events with the ASCII codes generated by the keypresses. Here's how to define the states:

Private Const S_OUTSIDE = 1 Private Const S_STARTING = 2 Private Const S_INSIDE = 3 Private Const S_ENDING = 4 Public nPuState As Integer 

Tip

If you're defining a group of constants to use as an enumerated type (you're effectively defining a State type here), always start the numbering at 1, not 0. This will help you spot uninitialized variables, since Visual Basic initializes integer variables to 0. Visual Basic 6 allows you to define enumerated types explicitly, but since they are freely interchangeable with longs, the same rule applies. (Unfortunately, none of this applies if you want to use your constants to index control arrays since the designers of Visual Basic chose to base them at 0.)

If you refer to the FSM tables for the comment stripper, you'll see that there are 12 different combinations of state and event, so your conditional logic needs to guide you along 12 different paths through the code. To implement this with simple conditional statements, you have the choice of using If-Then-ElseIf or Select Case statements; for this example, we'll arbitrarily choose the latter. To decode one particular path, the code will contain a fragment such as this:

Select Case nState     Case S_OUTSIDE:         Select Case nEvent             Case Asc("/")                 nState = S_STARTING             Case Asc("*")                 txtOutBox.Text = txtOutBox.Text & Chr$(nEvent)                 nState = S_OUTSIDE             Case Else                 txtOutBox.Text = txtOutBox.Text & Chr$(nEvent)                 nState = S_OUTSIDE         End Select     Case S_STARTING:     .     .     . End Select 

You can see that each of the 12 cells in the FSM tables has a piece of code inside a pair of nested Select Case statements. The State and Event tables are combined here, so the last statement in each case assigns a new value to nState (which we'll assume is a reference parameter). The rest of the code for each decoded state/event pair depends on what you want this particular implementation of the comment stripper to do—in fact, we're just going to add the text to the text box or not, so the actions here are simple. In practice, the code will usually be more manageable if you divide it up so that each state has its own function. Thus, the example above becomes something like this:

Select Case nState     Case S_OUTSIDE DoStateOUTSIDE(nState, nEvent)     Case S_STARTING DoStateSTARTING(nState, nEvent)     .     .     . End Select Sub DoStateOUTSIDE(ByVal niEvent As Integer, _                    ByRef noState As Integer)     Select Case niEvent         Case Asc("/")             noState = S_STARTING         Case Asc("*"):             txtOutBox.Text = txtOutBox.Text & Chr$(nEvent)             noState = S_OUTSIDE         Case Else             txtOutBox.Text = txtOutBox.Text & Chr$(nEvent)             noState = S_OUTSIDE     End Select End Sub 

Now you have the state variable and the logic for decoding the state/event pairs, and all you need is a source of events. In this example, you'll trap keypresses by setting the KeyPreview property of the form and generating an event for each keypress. All you need to do now is feed the events to the FSM by calling a function that contains the decoding logic (let's call it DoFSM). The keypress event handler looks something like this:

Private Sub Form_KeyPress(KeyAscii As Integer)     Call DoFSM(nPuState, KeyAscii)     KeyAscii = 0 ' Throw away the keypress End Sub 

In this example, the event codes and the real-world events that map onto them are one and the same—hence, the "action" code in each DoState routine can get the ASCII codes directly from the nEvent parameter. Most applications don't have such coupling, and you would need to arrange for any such real-world data to be buffered somewhere if you wanted the action routines to have access to it. Consider, for example, the Unix tool yacc (yet another compiler-compiler), which builds table-driven parsers that process sequences of tokens read from an input stream. A parser generated by yacc gets its tokens by successive calls to a C function named yylex(), which is the direct equivalent of the KeyPress event handler. The yylex() function returns a numeric token, equivalent to the nEvent parameter, but it also copies the full text of the actual word it recognized into a global variable named yytext. This variable is available to any code in the yacc-generated program.

The only element missing from the FSM program is something to initialize the state variable. Recall that one state of the FSM is always designated the start state, so you need a line of code to assign that to the state variable before you start generating events:

nPuState = S_OUTSIDE 

This can go in the Form_Load event of the comment stripper program. You'll find the source code for this program in CHAP13\fsm\simple\sim.vbp.

Recursion: See recursion

The comment stripper FSM works OK, but it has a dangerous flaw. It's a flaw that is inherent in event-driven systems, and one that also crops up in regular Visual Basic programs. The problem is reentrant code, and you might have come across it when working with data controls, Form_Resize events, or code that uses DoEvents.

Let's have a look at a simple example of reentrancy using a data control. The program shown in Figure 13-10 (which is in CHAP13\recurse\broken\rcb.vbp) is about as simple as it gets, with a single data-bound list wired up through a data control to the Visual Basic sample database BIBLIO.MDB (available on the Visual Studio 6 MSDN CD). Assume that the list contains a set of records you need to process somehow and that it doesn't matter in which order the records are processed. Clicking in the list causes a Reposition event, and the program puts up a message box that lets you simulate the kind of Jet page-locking error you might encounter in a multiuser application. You can think of the Reposition event handler as the equivalent of the DoFSM function in the comment stripper program.

Figure 13-10 Recursion in the data control's Reposition event

Clicking No when the message box pops up simply continues, and this is where you'd process the new record. Clicking Yes simulates a locking error and simply skips to the next record by calling the MoveNext method of the data control's recordset. The idea is that you'll reach the end of the locked page after skipping a few records and so find a record you can process. The problem here is that you're calling MoveNext from within the Reposition event handler, which causes another reposition event before the first one has finished—this is recursion. The example program maintains a static variable to count the number of recursions; the count is displayed in the message box, and the program also prints the entry and exit traces for the reposition event to the Immediate window when you run the program in the IDE. You can also see the effects of the recursion by pressing Ctrl+Break and selecting Call Stack from the View menu.

This example, which comes from a real program, might not have particularly serious consequences because it's a pure recursion that doesn't nest too deeply, and it involves no static data (except for the counter, of course). Generally, however, and particularly when you're devising code such as FSMs to control the loading and unloading of forms, the code will break as soon as you try to invoke it recursively. You might, for example, end up in a situation in which you're trying to load a form from its own Form_Load event.

Coming back to the recursive Visual Basic program, it's not immediately obvious how to fix the problem. It turns out that this is quite a common class of problem, and one that conveys the true flavor of event-driven code. What you want to do when you find a lock is to exit the event handler and then immediately issue a MoveNext on the recordset. Unfortunately, Visual Basic can't do this because as soon as you exit the event handler, control passes back to the run-time system (the <Non-Basic Code> you see when you select View/Call Stack in break mode). What you need to be able to do is to post some kind of request for a MoveNext and have it execute after you've left the Reposition event handler.

Just because Visual Basic won't do this kind of thing for you doesn't mean that you can't implement it yourself. CHAP13\recurse\fixed\rcf.vbp is a modified version of the pathological data control program that uses a simple event queue to achieve what you need. You use an unsorted list box as a convenient event queue and a timer control that continually polls the queue looking for events. There's only one kind of event in the program, so you don't even need to look at its value when you find it on the queue—always consider it a request for a MoveNext.

The program works like this: inside the Reposition event, instead of directly calling MoveNext when a locked record is encountered, we post an event onto the queue and then exit the event handler. The queue manager (the timer control) then comes along and, finding an event on the queue, kindly calls MoveNext for us. Now, however, the MoveNext is called from the timer's event handler, and there's no recursion. Notice that it doesn't matter how fast you push event requests into the queue; you never get recursion because the events are processed one by one in sequence.

Adding an event queue to an FSM

To prevent reentrant code, you need to add a queue to the FSM model. Strictly speaking, the comment stripper program doesn't need a queue because it doesn't do anything that will cause recursion. Because it's an example program, however, we'll add the queuing now so that you can build on it when you design real-world FSM programs later.

The queue built in the previous example worked adequately, but it needed a form to carry the list box and the timer control. This awkwardness over essentially nonvisual code has dogged Visual Basic from the start, and it means, for example, that you can't define a queue inside a class or a startup module without creating a dummy form. You could dump the controls onto an existing form, of course, but that's anathema to modular design, and it means you must contrive to load the form before starting the event queue. Getting rid of the list box isn't too hard, but until Visual Basic 5 there was no getting around that timer control without doing something horrific like this:

Sub Main()     Dim nEvent As Integer     frmMain.Show vbModeless    ' Main program is in here.     Do         If bGetEventFromQueue(nEvent) Then             DoFSM nPuState, nEvent         End If         DoEvents     Loop End Sub 

With Visual Basic 5 and 6, however, you can devise acceptable code-only solutions to this kind of problem—in this case, to build an event queue. By using the AddressOf operator, you can call the SetTimer API function and pass a Visual Basic routine as the timer's callback procedure. This means you can create a timer from pure code, and just like a Visual Basic Timer control, it will invoke the Visual Basic procedure asynchronously at the requested interval. Creating a timer is simple:

lTimerId = SetTimer(0&, 0&, 500&, AddressOf MyFunc) 

The first two parameters are NULL values, which simply signify that the timer isn't associated with any window, and the third is the timer interval, in milliseconds. The last parameter is the interesting one; it passes a pointer to a Visual Basic function that will be invoked by Windows whenever the timer fires. Windows expects this function to have the following interface and to pass the appropriate parameters:

Sub MyFunc(ByVal lHwnd As Long, _            ByVal nMsg As Long, _            ByVal lEventId As Long, _            ByVal lTime As Long) 

Note

When working with callback functions, be careful to include the ByVal keywords. If you miss a ByVal, simply moving your mouse pointer over the parameter name in the Visual Basic debugger is enough to crash Visual Basic. This happens because of Visual Basic 6's instant quick watch feature, which displays a variable's value as a ToolTip. Because Visual Basic thinks you passed a reference parameter (ByRef is the default), it tries to dereference an illegal pointer value, which almost always causes an access violation. You can turn off this feature with the Auto Data Tips check box under Tools/Options/Editor.

For now, just ignore the parameters. Make sure to destroy the timer when you're finished with it:

Call KillTimer (0&, lTimerId) 

That takes care of the queue manager, so now all you need to do is provide a queue for it to manage. A simple way to do this is to use a Visual Basic collection:

Dim colPuEventQueue As Collection 

You'll see a more sophisticated use of collections later, but for now you can use one as a simple queue by defining a couple of routines:

Sub AddEventToQueue(ByVal niEvent As Integer)     colPuEventQueue.Add niEvent End Sub Function bGetEventFromQueue(ByRef noEvent As Integer) As Boolean     If colPuEventQueue.Count = 0 Then         bGetEventFromQueue = False     Else         noEvent = colPuEventQueue.Item(1)         colPuEventQueue.Remove 1         bGetEventFromQueue = True     End If  End Function 

And that's it—a code-only asynchronous queue manager that you can build into a class or a normal module. The program CHAP13\fsm\qman\qman.vbp on the companion CD is the comment stripper FSM program amended to use the new event queue.

Building a Better Event Queue

Remember Message Blaster? Message Blaster is a custom control that lets you intercept Windows messages sent to any Visual Basic control. Windows messages are the raw material of Visual Basic events, but the Visual Basic designers filtered out most of the messages when they decided which events Visual Basic programmers were likely to need. A form's Resize event, for example, occurs after the resize has happened, which makes implementing size limits for a resizeable form ugly because you have to snap the size back in the Resize event handler. With Message Blaster, you can intercept the WM_SIZE message and change the form's size with a suitable API call before Windows repaints it.

Now that you know what Message Blaster is, forget it. Visual Basic 6 lets you do all the things that Message Blaster did, directly from Visual Basic code. Message Blaster is an example of a subclassing control; subclassing is what Windows programmers do to hook a custom message handler (usually called a window procedure) onto a window, and subclassing controls were an inelegant hack to make this possible in earlier versions of Visual Basic. By allowing Windows callback functions to be coded in Visual Basic, Visual Basic 6's AddressOf operator opens up subclassing directly to Visual Basic programmers.

The theory goes like this: You nominate any object that you have (or can get) a window handle for and tell Windows the address of a Visual Basic procedure to call whenever it receives a message for that object. For messages you don't want to handle, you simply call the original message handler. To fix the resizing problem outlined above, you'd write something like this:

pcbOldWindowProc = SetWindowLong(Me.hWnd, GWL_WNDPROC, _                                  AddressOf lMyWindowProc) . . . Function lMyWindowProc(ByVal hWnd As Long, _                        ByVal lMsg As Long, _                        ByVal wparam As Long, _                        ByVal lparam As Long) As Long     If lMsg = WM_SIZE Then         ' Play with the size here.     End If     lMyWindowProc = CallWindowProc(pcbOldWindowProc, hWnd, _                                    lMsg, wParam, lParam) End Function 

Any messages that Windows receives for a window are queued so that they arrive in sequence, and you can use this behavior to make a queue for FSMs. The simplest way is to hang a window procedure off an arbitrary control and start sending messages to the queue with PostMessage, but this is a bit ugly and can't be done unless you have a form loaded. A better way is to create a window for your own exclusive use behind the scenes. The code is straightforward:

lHwnd = CreateWindowEx(WS_EX_TRANSPARENT, "static", _                        "My Window", WS_OVERLAPPED, _                        0&, 0&, 0&, 0&, 0&, 0&, _                        CLng(App.hInstance), 0&) lEventMsg = RegisterWindowMessage("FSM Event") 

The choice of style and extended style parameters is arbitrary and doesn't really matter since you're never going to display the window. Now all you have to do is hook up an event handler to the window and start sending messages. It's a good idea to register a private message as done here, but you could just use any message number greater than WM_USER. It's best to encapsulate the code in Visual Basic functions or a class (CHAP13\fsm\fsmcls\pubfsm.cls shows one possible way), but be aware that the window procedure must be in a standard module. All the constants and Visual Basic declarations for all the functions can be pasted from the API Viewer tool supplied with Visual Basic. This tool is run from the file Apiload.exe, which is located in the Common\Tools\Winapi folder on the Visual Basic 6 CD.

Data-Driven Code

In an ideal implementation of a table-based design such as a finite state machine (FSM), the program is built from the tables themselves. In this kind of program, the tables are embedded in the code and somehow direct the flow of execution. The wisdom of this is clear: the tables are a product of your design process, and using them directly unifies the design—or at least some elements of it—with the code. It's also easier to make design changes because you don't have to translate between logical and physical models.

When it comes to building data-driven programs, working with more traditional Windows programming languages such as C and C++ offers two definite advantages over Visual Basic. First, you can maintain tables of pointers to functions and invoke those functions directly through indexes into the tables. This removes the need for the unwieldy jumble of conditional statements needed in our first stab at an FSM in Visual Basic, reducing the DoFSM function to just two statements:

void fvDoFSM(int nState, int *nEvent) {     (aapvActionTable[nState][*nEvent])();     nEvent = aanStateTable[nState][*nEvent]; } 

Second, you can lay out the tables in compile-time initialization statements. This is where the design and implementation intersect since you can lay out the table in a readable fashion and any changes you make to it are directly changing the code. Here's what the comment stripper FSM tables might look like in a C program:

void (*aapvActionTable[NUM_STATES][NUM_EVENTS])() = { //                E_SLASH     E_STAR     E_OTHER /* S_OUTSIDE  */ {fvOutSlash, fvOutStar, fvOutOther}, /* S_STARTING */ {fvStaSlash, fvStaStar, fvStaOther}, /* S_INSIDE   */ {fvInsSlash, fvInsStar, fvInsOther}, /* S_ENDING   */ {fvEndSlash, fvEndStar, fvEndOther} }; int aanStateTable[NUM_STATES][NUM_EVENTS] = { //                E_SLASH     E_STAR     E_OTHER /* S_OUTSIDE  */ {S_STARTING, S_OUTSIDE, S_OUTSIDE}, /* S_STARTING */ {S_STARTING, S_INSIDE,  S_OUTSIDE}, /* S_INSIDE   */ {S_INSIDE,   S_ENDING,  S_INSIDE}, /* S_ENDING   */ {S_OUTSIDE,  S_ENDING,  S_INSIDE} }; 

Unfortunately, although Visual Basic has an AddressOf operator, the only useful thing you can do with it is pass the address of a function or procedure in a parameter list. (C programmers will be disappointed to find that AddressOf isn't really like C's unary & operator.) Although you can use AddressOf in calls to Visual Basic functions, ultimately you can't do much inside those functions except pass the address on to a Windows API function. This capability is a major leap forward from all versions of Visual Basic previous to version 5, but the fact that you can't invoke a Visual Basic function from an address means that you can't implement an action table like the C one shown above.

Or can you? You can certainly store Visual Basic function addresses in a table by passing them to a suitable procedure. Visual Basic permits you to store function addresses in long variables:

Sub AddAddressToTable(ByVal niState As Integer, _                       ByVal niEvent As Integer, _                       ByVal pcbVbCodeAddr As Long)     ActionTable(niState, niEvent) = pcbVbCodeAddr End Sub 

Unfortunately, that's as far as you can go with pure Visual Basic. Perhaps a future version of Visual Basic will have a dereferencing operator or maybe a CallMe function that accepts an address and calls the function at that address; for now, however, you're on your own.

But don't despair, because you're not sunk yet. Visual Basic doesn't have a CallMe function, but there's nothing to stop you from writing your own. You'll need to write it in another language, of course, but if you're one of those Visual Basic programmers who breaks out in a cold sweat at the thought of firing up a C compiler, take heart—this is likely to be the shortest C program you'll ever see. Here's the program in its entirety:

#include <windows.h> BOOL WINAPI DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved) {     return TRUE; } void CALLBACK CallMe(void (*pfvVbCode)()) {     pfvVbCode(); } 

The business end of this code is a single statement; the DllMain function is scaffolding to make a DLL. (You also need to use a DEF file to make the linker export the CallMe symbol.) Now all you need to do is include a suitable Declare statement in your Visual Basic code, and you can call Visual Basic functions from a table!

Declare Sub CallMe Lib "callme.dll" (ByVal lAddress As Any) . . . CallMe ActionTable(nState, nEvent) 

The source code for the DLL and a Visual Basic program that calls it can be found in CHAP13\callme.

CallMe old-fashioned

The CallMe DLL is pretty simple, but it's still a DLL. It turns a programming project into a mixed-language development, it means you have to buy a compiler, and it adds an extra component to the distribution package you're going to have to build when you ship the product. Finding a way to do without a DLL would certainly be an attractive option.

Figuring out the answer simply requires a bit of lateral thinking. You've already seen how API functions that take a callback parameter can invoke Visual Basic functions, so it takes a simple shift of perspective to see such API functions as obliging CallMe servers. All you have to do is find an API function that takes a callback function, calls it once, and preferably doesn't do much else.

A quick trawl through the Win32 API documentation reveals SetTimer as a possibility since its sole purpose is to invoke an event handler that you register with it. The only problem with this is that SetTimer keeps calling the function until you kill the timer, so you must find a way to kill the timer after a single invocation. You could do this by including a call to KillTimer in the callback procedure itself, but this is ugly because the mechanism is inextricably bound up with the functions you want to call—if you're building an FSM, for example, all your action functions must look like this:

Sub Action1()     Call KillTimer lTimerId     ' Real action code goes here End Sub 

The consequence of leaving out a call to KillTimer is a ceaseless barrage of calls to the offending function, with who knows what consequences—yuck!

There are other candidates, but one that works nicely is CallWindowProc. This function is normally used to attach a custom message handler (a.k.a. a window procedure) to a window; the custom message handler passes on unwanted messages using CallWindowProc, which tells Windows to invoke the default window procedure. You're not chaining any message handlers here, and you don't even have a window; but you can still invoke CallWindowProc to call a Visual Basic function. The only restriction is that your Visual Basic function must have the following interface:

Function Action1(ByVal hWnd As Long, _                  ByVal lMsg As Long, _                  ByVal wParam As Long, _                  ByVal lParam As Long) As Long 

Windows 95 and Windows 98 let you call a parameterless procedure as long as you trap the "Bad DLL calling convention" error (error 491), but for reasons of portability—and good programming practice—you shouldn't rely on this.

All you need to do now is to wrap the CallWindowProc call up in a Visual Basic function, and you have a CallMe, effectively written in Visual Basic:

 Sub CallMe(ByVal pcbAddress As Long)     Call CallWindowProc(pcbAddress, 0&, 0&, 0&, 0&) End Sub 

Return of the comment stripper

It's time to return to the comment stripper. This time you're going to build a reusable FSM class using everything you've learned up to now—maybe you'll even pick up a few more tricks along the way. To see how the same FSM can be used to drive different external behaviors, you'll also make a slight modification to the program by displaying the text of the comments in a second text box. Figure 13-11 shows the new-look comment stripper. You can find the code in CHAP13\fsm\tabldriv on the companion CD.

Figure 13-11 Return of the comment stripper

First the bad news: you won't be able to match C's trick of laying out the FSM table readably in code. Visual Basic fights this on every front: you can't write free-format text, you run out of line continuations, there's no compile-time initialization, and even Visual Basic's comments aren't up to the job. However, this is the only bad news because using what you've learned about Visual Basic 6, you can do everything else the C program can do.

Let's start by looking at the interface to the FSM class. Since the class is to be general and you don't want to code the details of a particular FSM into it, you need to define methods that can be used to describe the FSM at run time. An FSM description will have four components: a list of states, a list of events, a table that defines state transitions, and a table that associates actions with the state transitions. In principle, the only other interface you need to the FSM class is a method you can call to feed events to the FSM. In practice, the restriction that demands that you put callback functions in a regular BAS file means you also need a method to register the event queue handler function with the FSM.

Here's what the run-time definition of the comment stripper FSM looks like:

Set oPiFSM = New CFSMClass oPiFSM.RegisterStates "OUTSIDE", "STARTING", "INSIDE", "ENDING" oPiFSM.RegisterEvents "SLASH", "STAR", "OTHER" oPiFSM.RegisterEventHandler cblEventQueueMessageHandler oPiFSM.TableEntry viState:="OUTSIDE", viEvent:="STAR", _                   viNewState:="OUTSIDE", _                   pcbiFunc:=AddressOf OutsideStar oPiFSM.TableEntry viState:="OUTSIDE", viEvent:="STAR", _                   viNewState:="OUTSIDE", _                   pcbiFunc:=AddressOf OutsideStar ' ...etc. 

This code shows how the states and events are defined and also includes a couple of the table-definition statements. RegisterEventHandler creates a hidden window to act as the event queue and installs the cblEventQueueMessageHandler function as its window procedure. We'll look at the table definitions in a moment, but first let's examine the RegisterStates and RegisterEvents methods. These work identically, so we'll take RegisterStates as an example.

To make the class general, you need to be able to supply this method with a variable number of arguments. There are two ways to do this, but ParamArray is the best. The definition of RegisterStates looks like this:

    Public Sub RegisterStates(ParamArray aviStates() As Variant)         ' Some code here     End Sub 

ParamArray members are Variants, which is convenient in this situation because the FSM class will allow you to choose any data type to represent states and events. The example program uses strings, mostly because they're self-documenting and can be displayed on the form. In real applications, you might prefer to use enumerated types or integer constants. Without making any changes to the class definition, you could define your states like this:

Const S_OUTSIDE = 1 Const S_STARTING = 2 Const S_INSIDE = 3 Const S_ENDING = 4 . . . oPiFSM.RegisterStates S_OUTSIDE, S_STARTING, S_INSIDE, S_ENDING 

Or like this:

Enum tStates     Outside = 1     Starting     Inside     Ending End Enum . . . oPiFSM.RegisterStates Outside, Starting, Inside, Ending 

Enumerated types were introduced in Visual Basic 5. In use they are equivalent to long constants defined with Const. Enumerations are better because they associate a type name with a group of constants, so in this example you can define variables of type tStates (although there is no run-time range checking). A more important difference is that you can define public enumerated types inside classes, which means you can now associate groups of constants directly with classes. If you were coding a comment stripper FSM class (instead of a general class that we'll use to implement the comment stripper), for example, you could define public tStates and tEvents as enumerated types in the class itself.

The FSM class can cope with any data type for its states and events because internally they are stored as integers and use collections to associate the external values with internal ones.

Here's the code behind RegisterStates:

Private Type tObjectList     colInternalNames As New Collection     colExternalNames As New Collection End Type Private tPiList As tObjectList . . . tPiList.colInternalNames.Add nInternId, key:=CStr(vExternId) tPiList.colExternalNames.Add vExternId, key:=CStr(nInternId) 

This code creates two reciprocal collections: one storing integers keyed on external state names and the other storing the names keyed on the integers. You can now convert freely between internal (integer) and external (any type) states. Since you can store any data type in a collection, you are free to choose whichever data type is most convenient.

Tip

Using pairs of collections is a powerful way to associate two sets of values. Usually, one set is how the values are represented in a database and the other set is how you want to display them to the user.

The FSM table itself is created dynamically inside the RegisterStates or RegisterEvents routine (whichever is called last), using the Count properties of the state and event collections for its dimensions:

Private Type tTableEntry     nNextState As Integer     pcbAction As Long End Type . . . ReDim aatPiFSMTable(1 To nStates, 1 To nEvents) As tTableEntry 

Now you need to fill in the empty FSM table with details of the state transitions and actions. To do this, you make repeated calls to the TableEntry method, with one call for each cell in the table. The values you want to insert into the table are successor states, which have one of the values defined earlier in the state list, and subroutine addresses, which you obtain with the AddressOf operator. The action routines are all parameterless subroutines, defined together in a single BAS file. Here's what the TableEntry method does:

aatPiFSMTable(nState, nEvent).nNextState = niNewState aatPiFSMTable(nState, nEvent).pcbAction = pcbiFunc 

The nState and nEvent integers are first obtained by looking up the external names passed as parameters.

Once the table is in place, the FSM is ready to go. In fact, the FSM is running as soon as you define it since RegisterEventHandler creates an event queue and registers a callback function to service it. RegisterStates puts the FSM into its start state, but it won't actually do anything until you start feeding events to it.

The event queue is implemented as an invisible window created with Windows API functions as described earlier. The only minor problem here is that Visual Basic insists that you define callback functions in normal BAS files, so you can't include the queue event handler in the class definition. You can almost do it because you can define the event handler in the class as a Friend function; the function you register is a simple shell that calls the Friend function, although it still has to be in a normal BAS file. The class must contain the following function.

Friend Function cblEvHandler (     ByVal hwnd As Long, _     ByVal lMsg As Long, _     ByVal wparam As Long, _     ByVal lparam As Long ) As Long 

This is a standard window procedure (don't forget the ByVals!), and you send events to it using the PostMessage API function. A Friend function is essentially a public method of the class, but the scope is limited to the current project even if the class is defined as Public. A call to PostMessage is the essence of the PostEvent method, and Windows arranges for the messages to be delivered asynchronously, via calls to the cblEvHandler function, in the sequence they were posted.

Calls to PostEvent are made in response to external stimuli, and in this case these are all Visual Basic keypress events. The calls are made from the KeyPress events, where the translation from ASCII code to an appropriate event value ("STAR", for example) is made. After the FSM is initialized, the KeyPress events are the only interface between the FSM and the outside world.

The queue event handler is the focus of the FSM since here is where the table lookup is done and the appropriate action procedure is called:

CallMe aatPiFSMTable(nPiCurrentState, wparam).pcbAction nPiCurrentState = aatPiFSMTable(nPiCurrentState, wparam).nNextState 

The only other noteworthy feature of the queue event handler is that it contains calls to RaiseEvent. The FSM class defines four different events that can be used in the outside world (the comment stripper program in this case) to keep track of what the FSM is doing. These are the events:

Event BeforeStateChange(ByVal viOldState As Variant, _                         ByVal viNewState As Variant) Event AfterStateChange(ByVal viOldState As Variant, _                        ByVal viNewState As Variant) Event BeforeEvent(ByVal viEvent As Variant) Event AfterEvent(ByVal viEvent As Variant) 

You saw an example of RaiseEvent earlier; this time, you're defining events with parameters. You define two sets of events so that you can choose whether to trap state changes and events before or after the fact. For the comment stripper, use the AfterEvent and AfterStateChange events to update the state and event fields on the form.

Doing it for real

The comment stripper is a simple example, and the FSM it demonstrates doesn't deal with window management. As a slightly more realistic example, let's look at an implementation of the GUI from Figure 13-5. You'll find the source for this program in CHAP13\fsm\realwrld\rlw.vbp. The FSM controls the hypothetical Function 1, and the FSM starts when that function is chosen from the Function menu. Other functions would be implemented with their own FSMs, which is straightforward because the FSM was built as a class. You're not really implementing the whole program here, just the window-management parts; all the event routines are there, so adding the code to do the database actions would be painless.

The second thing you'll notice, right after you notice those bizarre event names, is that the nice, friendly action routine names have gone, replaced by the anonymous subroutines a01 through a44. With 44 subroutines to code, the only sensible names are systematic ones—using the state and event names as before is just too unwieldy. In fact, the action names are irrelevant because their corresponding state/event combinations are much more useful identifiers. Here's a portion of the FSM table definition:

oPuFSM.TableEntry A__, A_Ok_____, EXI, AddressOf a01 oPuFSM.TableEntry A__, A_Cancel_, EXI, AddressOf a02 oPuFSM.TableEntry A__, A_Apply__, A__, AddressOf a03 oPuFSM.TableEntry A__, A_Details, AB_, AddressOf a04 oPuFSM.TableEntry A__, A_More___, AC_, AddressOf a05 oPuFSM.TableEntry A__, B_Ok_____, ERO oPuFSM.TableEntry A__, B_Cancel_, ERO 

The key description of this code is "systematic," which is also why we've adopted such a strange convention for the state and event names. We're fighting Visual Basic's unreasonable layout restrictions by making the names the same length so that the list of TableEntry calls is readable. You can't quite make a table layout as in the C code example earlier, but the result is an acceptable facsimile that is reasonably self-documenting.

Notice that two pseudostates have been introduced for this example: EXI, which represents termination of the FSM, and ERO, which denotes an error condition. Neither of these conditions should be encountered by the FSM: EXI successor states are never reached because the action routines associated with their transitions halt the FSM, and ERO successor states can be derived only from illegal inputs. The FSM driver function (oPuFSM.EvHandler) traps these pseudostates and raises an FSM_Error event. This is the FSM equivalent of a Debug.Assert statement.

The use of ERO states also permits you to omit coding for state transitions that will never happen. As well as modifying the driver to raise an error on illegal transitions, we've also modified the TableEntry method to make the action function optional. In this case, it saves 12 action functions and nicely distinguishes error conditions in the matrix. It's tempting to omit these lines from the list, but you should avoid the temptation vigorously, because if you do so you can no longer tell whether you've covered all possible situations by simply counting the table entries.

Another temptation is to factor code by reusing action routines—for example, a01 and a02 appear to be the same, as do a12 and a13. However, discarding a02 and wiring up a01 in its place can be disastrous because it introduces a dependency that will cause problems if you later want to change the actions for either transition independently of the other. You could, of course, define a helper subroutine that's called by both action routines. (ConfirmDiscardEdits is such a function.) Remember that a system is useful because it takes some of the intellectual load off managing complexity, and it goes without saying that circumventing the system—for whatever reason—stops it from being systematic.

One final comment about this example is that it doesn't include validation or confirmation states. Such states would amplify the complexity by adding a new state for each OK and Cancel event, along with 11 corresponding table entries (in this case). In real life, validation and confirmation are best handled by building a conditional mechanism into the FSM. This does not mean you should do such processing ad hoc, and control over the successor state should remain with the FSM driver function (FSM.EvHandler). This means you can't use Visual Basic's Form_QueryUnload or Form_Unload event to trigger validation or confirmation since a form unload must always succeed. (Canceling an unload from QueryUnload will cause havoc because the FSM thinks the form has been unloaded and now its state information is incorrect.)

An acceptable way to implement both types of condition is to add an abort transition method to the FSM class:

Public Sub AbortTransition()     bPuTransitionAborted = True End Sub 

Now you can modify the FSM driver to check the bPuTransitionAborted flag before setting the successor state:

Public Sub EvHandler     .     .     .     CallMe aatPiFSMTable(M_nCurrentState, wparam).pcbAction     If Not bPuTransitionAborted Then         nPiCurrentState = aatPiFSMTable(nPiCurrentState, _                                         wparam).nNextState     End If     .     .     . End Sub 

This might be simple, but it adds considerable complexity to the action routines because you must be very careful about which forms you unload. More specifically, if you cancel a transition, you need to be sure that you don't change anything that characterizes the current state. In this case, the states are defined entirely in terms of forms, so you need to ensure that the action routine has the same forms loaded when you leave that were loaded when you entered. For example, assuming you're in state AB_ (forms A and B loaded), you need either to unload both forms or to leave them both loaded. The following code correctly describes the validation logic for an A_Ok event in this state:

Public Sub a12()     Dim bUnload As Boolean     bUnload = True     If frmDetails.My.Dirty Or frmSummary.My.Dirty Then         If Not bConfirmDiscardEdits Then             bUnload = False         End If     End If              If bUnload Then         Unload frmDetails         Unload frmSummary     Else         oPuFSM.CancelTransition     End If      End Sub 


Ltd Mandelbrot Set International Advanced Microsoft Visual Basics 6. 0
Advanced Microsoft Visual Basic (Mps)
ISBN: 1572318937
EAN: 2147483647
Year: 1997
Pages: 168

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net