Visual Basic

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

After the Dust Has Settled

Visual Basic was an innovation. A few years ago, the Windows programming club was an exclusive one, and coding for Windows was intensely technical: virtuosity in C was the entrance requirement, and becoming productive relied on mastery of the arcane Windows API. Visual Basic changed all that, opening up Windows programming to all comers and pioneering whole new development cycles by making rapid GUI prototyping a reality.

But there is a darker side. By eliminating the obscure programmatic hoops we must jump through even to display anything on the screen, Visual Basic has taken the technical edge off Windows development, and from the wrong perspective this can have dangerous consequences. Behind the GUI facade, developers face the same problems of design, verification, construction, redesign, testing, and change management that they always have, and without conscientious technical management, these fundamentals can take a back seat while the product is "prototyped" to market.

To ensure success in a Visual Basic project, you need to concentrate on development fundamentals as much as on the database design and graphical veneer, and you must quash unreasonable productivity expectations. Visual Basic is a tinkerer's delight, but the delusion of Visual Basic programming as child's play is short-lived and a recipe for disaster. A Visual Basic project can seem like a whirlwind of fantastic productivity-for the first few months. Only after the dust has settled is the truth apparent: excellent applications happen by design, not by accident.