Most applications use message boxes for providing reactive feedback, but they have some serious drawbacks in the “heads down” data entry environment. In this article, Rebecca Riordan demonstrates a technique that replaces message boxes with sound and status bar text messages.
Just about every application needs to implement some type of reactive feedback. At the very least, you need to notify your users when a command fails; sometimes you need to reassure them that it has succeeded. Message boxes are the traditional mechanism for providing this feedback, but they have some drawbacks, particularly in a data entry environment where the user is unlikely to be looking at the screen.
What sort of problems? First, because message boxes always have a default button, they can be dismissed without the user ever even knowing they’re there. Any keystrokes other than Space or Enter are simply ignored by the message box, and many keys (Space, Enter, Escape, shortcut keys) make the message box disappear completely. This is a recipe for user confusion, as any touch-typist can confirm.
Second, even if the message box is seen by the user, it disappears as soon the user acknowledges it. Now, what was that message again? Which field had the problem?
Finally, because the message box interrupts the user, you must use it sparingly. For example, it wouldn’t be practical to display a message box after the user exits each field to confirm that the data is valid. Not only would that add an extra keystroke to each field, it would increase the chance that the user would dismiss an error notification without seeing it.
Many, if not most, developers sound a beep before they display a message box, particularly if the message box is being used to notify the user of an error. This goes some way toward addressing the problems with this feedback mechanism, but it doesn’t really resolve them. It’s the nature of the Windows message loop that the users can still dismiss the message box before they consciously process the sound and realize that there is (or was) a message for them to see.
But while the single, standard, lonely beep that means “I’m going to show you a message” isn’t enough to stop the touch-typist before he causes the message to disappear, the “beep technique” does point the way to an effective alternative to message boxes. If you substitute two non-standard sounds, one meaning (roughly) “okay” and the other meaning “not okay,” you can dispense with the message box completely, provide more complete feedback, and avoid disrupting the user. The appearance of the new “not okay” sound in the midst of a series of “okay” sounds attracts the user’s attention more quickly than the single, standard Windows beep. The other step is to use the Access status bar to display the message. Messages stay in the status bar, and users can view the message while continuing with their work.
VBA only supports a single sound intrinsically: the beep. If you’re going to use sound as a feedback mechanism, not only must you have two distinct sounds, but they must both be far subtler than the beep. The solution is to use the intrinsic Windows ability to play wave files. Following is the code necessary to call the sndPlaySound function in the winmm (“Windows Multimedia”) library:
Declare Function sndPlaySound Lib "winmm.dll" _ Alias "sndPlaySoundA" _ (ByVal lpszSoundName As String, _ ByVal uFlags As Long) As Long Public Sub PlaySoundFile(ByVal fileName As String) Dim x As Integer x = sndPlaySound(fileName, 1) End Sub
The sndPlaySound function is the simplest of the Windows functions for playing sounds, and perfectly adequate for playing the sounds required for feedback. If you need more sophisticated functionality, however, you can use the PlaySound function. The technique is similar, but the function is more powerful. If you want, you can even instantiate an instance of the Windows Media Player to play your sound. I’ll refer you to MDSN or the Microsoft Web site for programming details and examples of these other techniques.
Once I’ve declared the function, I’ve wrapped it in a public function that accepts the name of a wave file and plays it asynchronously (meaning that your code will continue executing while the sound is playing). You can put the declaration and function in the code-behind module of your code, or in a standard module. A standard module is generally a better idea because it allows you to play wave files throughout your application.
Having created the module, you can implement your sound scheme. Figure 1 shows a simple form that I’ll use for demonstration purposes (it’s included in the accompanying Download file). I’ll admit that, as a data input form, this one is pretty awful. In a real application, you’d never use text boxes for forms that only allow two possible values–the text box control is much too vague. That very vagueness makes it easy to demonstrate how sound can be used, but take this form as a demonstration, please, not as an example of good practice.
The first step is to choose your sounds. The trick here is that they need to be short, distinctive, and unobtrusive. Think of the noise that a key makes when you press it. It’s unobtrusive, and yet you notice immediately when it’s absent. That’s what you’re aiming for. Now think of a room full of order entry operators running an application that beeps after every field. That’s what you’re trying to avoid.
The following code shows the Form_Load event handler from our demonstration form. The procedure first declares a string variable, path, and sets it to the location of the database. The procedure then sets the values of three variables, sndFldOK, sndRecOK, and sndFail, to the name of wave files. The wave files I’m using in the sample are all included with Windows, and available in the Windows\Media folder by default. My function assumes that these files have been copied to the folder containing the database. It’s important to be precise when you pass a file name to the sndPlaySound function: If the file isn’t found, the function will play a default sound, which can confuse your users:
Private Sub Form_Load() Dim path As String path = Application.CurrentProject.path sndFldOK = path & "\ding.wav" sndRecOK = path & "\convert.wav" sndFail = path & "\notify.wav" End Sub
The three sound variables are declared at the module level so that they’ll be available to all of the validation functions for the form (the declarations aren’t shown in the sample listing). If you’re using the same files for multiple forms, you might include them as public variables in the module in which you declare the PlaySoundFile function.
The next step is to implement your validation functions. The following code shows the function for the YesOrNo field. The field requires that the user type either “Yes” or “No” and checks for it in the BeforeUpdate event handler:
Private Sub YesOrNo_Exit(Cancel As Integer) If Me.YesOrNo = "Yes" Or Me.YesOrNo = "No" Then PlaySoundFile (sndFldOK) SysCmd acSysCmdClearStatus Else PlaySoundFile (sndFail) SysCmd acSysCmdSetStatus, "Please enter 'Yes' or 'No'" Cancel = True End If End Sub
My simple BeforeUpdate consists of a single If…Else…End If block. If the field contains valid data, the procedure calls the PlaySoundFile function, passing the sndFldOK file as an argument. The code then calls the SysCmd method to clear any status bar text.
If the data fails validation, the sndFail sound is played, an error message is posted to the status bar, and the Cancel argument of the event handler is set to True, which prevents the user from exiting the control. The status bar can be used here because the sound will notify the user of the problem; you don’t need the visual interruption of the message box. You could, of course, use a message box if you prefer, or use a message box in conjunction with setting the status bar text. In fact, if you’re concerned that the user may not hear the notification sound, you may be more comfortable with a message box here. The message will stay in the status bar for the user to view it at her convenience.
Obviously a real application would have more complex validation texts, but the basic principle would be the same–if the value passes, play the OK sound, otherwise play the Fail sound. The sample code included with this article provides some other examples.
The following code shows how to handle a control that can only contain valid input, either because the underlying field can contain any type of data or because the control that represents it on the form constrains the user to valid entries. Because there tend to be a lot of these “no-validation” controls, the procedure simply calls a sub procedure, NoValidation, which plays the OK sound and clears the status bar. You could replace the relevant lines of the previous code with a call to the procedure as well, saving yourself a line of typing for each control:
Private Sub aOrB_Exit(Cancel As Integer) NoValidation End Sub Private Sub NoValidation() PlaySoundFile (sndFldOK) SysCmd acSysCmdClearStatus End Sub
The key to using sound as an effective feedback mechanism (as with so many user interface techniques) is to use it consistently. In a data entry environment, this means you must use the “okay” sound after every valid entry. It’s a judgment call whether “valid entry” means each field, each record, or both. I tend to use both field and record since there are invariably both field-level and record-level validation rules, but it’s probably clear by now that field-level confirmations can be tedious to implement. Not difficult, but tedious, since you must add a BeforeUpdate event handler for every control on the form (or, at least, every control that accepts data input). Just tell yourself that cut-and-paste is a wonderful thing.
There’s another problem in using this technique. Access processes the expression specified in the ValidationRule property of the control before it calls the BeforeUpdate event. If the value the user has entered violates that rule, Access will sound the standard Windows beep and display a message box–precisely the behavior you’re trying to avoid.
Of course, bound controls will inherit the ValidationRule property of the fields on which they’re based, so if you’re using table-level validation (and you should), you’ll have to manually delete the ValidationRule property after you create the BeforeUpdate event. In practice, this isn’t a great problem, even in maintenance. If the rule changes at the table level, you’ll need to update the BeforeUpdate event to reflect the change anyway. It really is simply a matter of deleting the rules when you first create a bound form (this problem doesn’t apply to unbound forms).
While sound feedback requires some extra work on the part of the programmer, the work isn’t onerous, and it has the useful side effect of forcing you to consider the validation rules for every control on the form. You should do this anyway, of course, but even the best analysts occasionally overlook things. On the plus side, the technique provides a truly effective and non-obtrusive feedback mechanism for data entry. My clients have found this to be a more-than-fair trade in many situations; yours probably will too.