Bypassing the events from hell
Visual Basic's GotFocus and LostFocus events have always been exasperating to Visual Basic programmers. They don't correspond to the normal KillFocus and SetFocus messages generated by Windows; they don't always execute in the order that you might expect; they are sometimes skipped entirely; and they can prove very troublesome when you use them for field-by-field validation.
Microsoft has left these events alone in Visual Basic 6, probably for backward compatibility reasons. However, the good news is that the technical boys and girls at Redmond do seem to have been hearing our calls for help. Visual Basic 6 gives us the Validate event and CausesValidation property, whose combined use avoids our having to use the GotFocus and LostFocus events for validation, thereby providing a mechanism to bypass all the known problems with these events. Unfortunately, the bad news is that the new mechanism for field validation is not quite complete.
Before we dive into Validate and CausesValidation, let's look at some of the problems with GotFocus and LostFocus to see why these two events should never be used for field validation. The following project contains a single window with two text box controls, an OK command button, and a Cancel command button. (See Figure 6-1.)
Figure 6-1 Simple interface screen hides events from hell
Both command buttons have an accelerator key. Also, the OK button's Default property is set to TRUE (that is, pressing the Enter key will click this button), and the Cancel button's Cancel property is set to TRUE (that is, pressing the Esc key will click this button). The GotFocus and LostFocus events of all four controls contain a Debug.Print statement that will tell you (in the Immediate window) which event has been fired. This way we can easily examine the order in which these events fire and understand some of the difficulties of using them.
When the application's window is initially displayed, focus is set to the first text box. The Immediate window shows the following:
Program initialization txtBox1 GotFocus
Just tabbing from the first to the second text box shows the following events:
txtBox1 LostFocus txtBox2 GotFocus
So far, everything is as expected. Now we can add some code to the LostFocus event of txtBox1 to simulate a crude validation of the contents of txtBox1, something like this:
Private Sub txtBox1_LostFocus Debug.Print "txtBox1_LostFocus" If Len(txtBox1.Text) > 0 Then txtBox1.SetFocus End If End Sub
Restarting the application and putting any value into txtBox1 followed by tabbing to txtBox2 again shows what looks like a perfectly normal event stream:
txtBox1_LostFocus txtBox2_GotFocus txtBox2_LostFocus txtBox1 GotFocus
Normally, however, we want to inform the user if a window control contains anything invalid. So in our blissful ignorance, we add a MsgBox statement to the LostFocus event of txtBox1 to inform the user if something's wrong:
Private Sub txtBox1_LostFocus Debug.Print "txtBox1_LostFocus" If Len(txtBox1.Text) > 0 Then MsgBox "txtBox1 must not be empty!" txtBox1.SetFocus End If End Sub
Restarting the application and putting any value into txtBox1 followed by tabbing to txtBox2 shows the first strangeness. We can see that after the message box is displayed, txtBox2 never receives focus-but it does lose focus!
txtBox1_LostFocus txtBox2_LostFocus txtBox1 GotFocus
Now we can go further to investigate what happens when both text boxes happen to have invalid values. So we add the following code to the LostFocus event of txtBox2:
Private Sub txtBox2_LostFocus Debug.Print "txtBox2_LostFocus" If Len(txtBox2.Text) = 0 Then MsgBox "txtBox2 must not be empty!" txtBox2.SetFocus End If End Sub
Restarting the application and putting any value into txtBox1 followed by tabbing to txtBox2 leads to a program lockup! Because both text boxes contain what are considered to be invalid values, we see no GotFocus events but rather a continuous cascade of LostFocus events as each text box tries to claim focus in order to allow the user to change its invalid contents. This problem is well known in Visual Basic, and a programmer usually gets caught by it only once before mending his or her ways.
At this point, completely removing the MsgBox statements only makes the situation worse. If you do try this, your program goes seriously sleepy-bye-bye. Because the MsgBox function no longer intervenes to give you some semblance of control over the event cascade, you're completely stuck. Whereas previously you could get access to the Task Manager to kill the hung process, you will now have to log out of Windows to regain control.
These are not the only peculiarities associated with these events. If we remove the validation code to prevent the application from hanging, we can look at the event stream when using the command buttons. Restart the application, and click the OK button. The Immediate window shows a normal event stream. Now do this again, but press Enter to trigger the OK button rather than clicking on it. The Debug window shows quite clearly that the LostFocus event of txtBox1 is never triggered. Exactly the same thing happens if you use the OK button's accelerator key (Alt+O)-no LostFocus event is triggered. Although in the real world you might not be too worried if the Cancel button swallows a control's LostFocus event, it's a bit more serious when you want validation to occur when the user presses OK.
The good news with Visual Basic 6 is that you now have a much better mechanism for this type of field validation. Many controls now have a Validate event. The Validate event fires before the focus shifts to another control that has its CausesValidation property set to True. Because this event fires before the focus shifts and also allows you to keep focus on any control with invalid data, most of the problems discussed above go away. In addition, the CausesValidation property means that you have the flexibility of deciding exactly when you want to perform field validation. For instance, in the above project, you would set the OK button's CausesValidation property to True, but the Cancel button's CausesValidation property to False. Why do any validation at all if the user wants to cancel the operation? In my opinion, this is a major step forward in helping with data validation.
Note that I stated that "most" of the problems go away. Unfortunately, "most" is not quite "all." If we add Debug.Print code to the Validate and Click events in the above project, we can still see something strange. Restart the application, and with the focus on the first text box, click on the OK button to reveal a normal event stream:
txtBox1 Validate txtBox1 LostFocus cmdOK GotFocus cmdOK Click
Once again restart the application, and again with the focus on the first text box, press the accelerator key of the OK button to reveal something strange:
txtBox1 Validate cmdOK Click txtBox1 LostFocus cmdOK GotFocus
Hmmm. The OK button's Click event appears to have moved in the event stream from fourth to second. From the data validation point of view, this might not be worrisome. The Validate event still occurs first, and if you actually set the Validate event's KeepFocus argument to True (indicating that txtBox1.Text is invalid), the rest of the events are not executed-just as you would expect.
Once again, restart the application and again with the focus on the first text box, press the Enter key. Because the Default property of the OK button is set to True, this has the effect of clicking the OK button:
Oops! No Validate event, no LostFocus or GotFocus events. Pressing the Escape key to invoke the Cancel button has exactly the same effect. In essence, these two shortcut keys bypass the Validate/CausesValidation mechanism completely. If you don't use these shortcut keys, everything is fine. If you do use them, you need to do something such as firing each control's Validate event manually if your user utilizes one of these shortcuts.
Some final thoughts Never rely on GotFocus and LostFocus events actually occurring-or occurring in the order you expect. Particularly, do not use these events for field-by-field validation-use Validate and CausesValidation instead. Note that the Validate event is also available for UserControls.