Autumn 91 - VALIDATING DATE AND TIME ENTRY IN MACAPP
VALIDATING DATE AND TIME ENTRY IN MACAPP
JAMES PLAMONDON
MacApp's TEditText class checks strings entered by the user, displaying an error
message when an invalid string is encountered. This article shows how TEditText's
validation and error notification schemes can be made more flexible, and demonstrates
this flexibility in TEditText subclasses for the entry of dates and times. You can try out
these classes in the sample program that's on the Developer CD Series disc.
My favorite high school teacher, Mrs. Whalen, had a sign under the wall clock in her
classroom that read "Time passes--but will you?" Back when I was in the Class of
'78, there were many times I wished that I could set the clock back (during a tough
quiz) or forward (on a warm afternoon). Although I can't offer any such hope to the
Class of '91, I can at least provide MacApp developers with classes that make the entry
of dates and times as easy as I ever dreamed.
I wrote these classes during some recent work on a MacApp application that involved
the entry and validation of dates and times. After considering and rejecting all sorts of
controls--controls that looked like little monthly calendars, 24-hour clocks, and so
on--I settled on simple editable text boxes. I thought that with these boxes, those
pesky localization issues that plagued the other designs wouldn't be a problem, because
I could use the Macintosh Script Manager to handle the different date and time formats
described by the international resources in the operating system. I also figured that if
I used MacApp's TEditText class, writing editable text boxes for date and time entry
would be trivial. An override here, a little data there, and voilà--done. It
wasn't the first time I've been wrong.
But to understand TEditText's flaws, first you have to know how it works.
TEDITTEXT REVEALED
TEditText is a TControl subclass. It encapsulates the Toolbox's TextEdit routines. A
TEditText view is to one of MacApp's TDialogViews what an editText item is to one of the
Dialog Manager's dialogs: it allows the user to enter strings into a box in a dialog box.
In addition, TEditText extends the functionality of editable text items to include the
notion of validation. If an invalid string is entered into a TEditText view, an alert is
displayed, notifying the user of the problem.
The validation process implemented by TEditText centers on its Validate method. In
TEditText, a valid string is any string that's not longer than the maximum allowed
length, which is specified by the application's author. If the string is valid, the
Validate method returns the constant value kValidValue; otherwise--that is, if the
string is too long--it returns the error code kTooManyCharacters. TNumberText, a
subclass of TEditText that handles the entry and validation of integer numbers, can
return additional error codes--kValueTooSmall, kValueTooLarge, or
kNonNumericCharacters.
Figure 1 MacApp's Validation Error Alert
The only place Validate is ever called in MacApp is from the TDialogView method
DeselectCurrentEditText. If Validate returns a value other than kValidValue, that value
is assumed (in a call to TDialogView.CantDeselect) to be an index into a string list
resource called kInvalidValueReasons. It's expected that the string at that index will
describe the error encountered. This string is then displayed in an error alert that
tells the user why the string entered is invalid. Figure 1 shows the alert displayed
when the user types too many characters into a TEditText view.
My dad used to say "Whenever a guy's telling you what he's gonna dofor you, start
worrying about what he's gonna doto you." It wasn't long before I realized that
TEditText was like that. I had hoped that it would be easy to extend the checking done in
TEditText.Validate to include checking for a valid date or time, but it wasn't. To add this
kind of checking, I was going to have to rewrite Validate from scratch-- just the kind
of thing object-oriented programming is supposed to prevent.
DON'T FIX WHAT AIN'T BROKE
When in the course of application programming it becomes necessary to replace a
mechanism written by the MacApp engineers, one should declare the causes that impel
this decision. I hold these truths to be self-evident:
1. That the reuse of existing code is preferable to the addition of new code.
2. That the addition of new code is preferable to the alteration of existing
code.
3. That the alteration of existing code is preferable to missing a deadline.
MacApp's approach to text validation fails to meet a number of these criteria. First, it
assumes that new error strings will simply be added to the STR# resource called
kInvalidValueReasons, with new error codes indexing the added strings. However, this
won't work: TDialogView.CantDeselect uses a constant, kNoOfDefaultReasons, to
indicate the number of strings in this resource. It can only be changed by altering and
recompiling MacApp--a violation of self-evident Truth #2.
Also, the error-code-equals-string-index scheme can be a problem when one
combines existing class libraries; two different TEditText subclasses, written
independently, may use the same error codes (and string indices) to indicate different
problems. Resolving this conflict would probably require changing and recompiling at
least one of the conflicting classes.
Further, the use of error strings can cause problems during localization since not all
languages can stick an arbitrary string into a sentence and have the result make any
sense. Static error strings also give little context--they may not be able to display the
invalid string, or a valid example string, to help the user figure out what went wrong.
For all of these reasons, MacApp's use of a single error string list--with Validate's
result being used as an index into this list--seems inappropriate. Each class should
instead build its own error strings in any manner it sees fit, using its own string lists
as necessary.
That's not all. The error alert displayed when invalid strings are encountered has only
one button. But what if two or more alternative actions can be taken in response to
the entry of an invalid string?
Consider the following validation case (which has nothing to do with dates or times).
Assume that the user needs to enter the name of a category--like Work, School, or
Personal--into an editable text box. If the string the user enters matches the name of
an existing category (for example, "Work"), the string is valid; otherwise--for
example, if the user types "Wirk"--the string is invalid.
Figure 2 Two-Option Validation Error Alert
In addition, we want to allow the user to add new categories to the list by entering
them into the editable text box. To do this, we must distinguish those entries that are
simply mistyped (like "Wirk") from those intended to become new category names
(like "School" or "Personal"). In effect, we need to present the user with a two-button
dialog box like that in Figure 2.
Unlike the default MacApp validation alert, which has only one button, the dialog box
in Figure 2 allows the user to decide whether the entered string--"Personal," in this
case--is valid or invalid.
TVALIDTEXT: THE UNAUTHORIZED BIOGRAPHY
So to extend validation to dates and times, I decided to write two new classes,
TDateEditText and TTimeEditText. After writing these classes, I realized that they had
so much validation code in common that it made sense to put this code in a common
superclass. I called this superclass TValidText.
TValidText is a pretty simple extension to TEditText. It adds three notions to
TEditText-- strictness, required value, and an invalid entry alert ID. It also
significantly enhances the text validation process.
TValidText is an example of an "abstract class"--a class that's never expected to be
instantiated directly. It exists only to factor out the code that's expected to be common
to its subclasses. All of TValidText's subclasses will simply inherit its validation and
error reporting code, while overriding a few methods to implement their own specific
validation tests and error messages.
The class declaration for TValidText is as follows:
TValidText = OBJECT(TEditText)
fStrict: BOOLEAN;
fRequired: BOOLEAN;
fAlertID: INTEGER;
.
. See the Developer CD Series disc for method declarations.
.
TValidText's fStrict field, a Boolean variable, determines whether or not strict
checking will be used when validating. This field exists here because both the date and
time classes needed the concept. TValidText itself doesn't use fStrict, except to get and
set its value. It might be more general to implement it as a scalar (maybe a signed
byte) to provide multiple strictness levels. We'll look at strictness again in the
discussion of the date and time classes later in this article.
The fRequired field answers the question of whether an empty string is valid or not. As
far as TValidText is concerned, if fRequired is true, an empty string is invalid;
otherwise, it's valid. TValidText's subclasses may add additional conditions to the
notion of validity by overriding the method IsValid and calling the inherited version.
Both the date and time editing classes do this, as we'll see later.
The fAlertID field contains the resource ID of the alert to be displayed when the
current text doesn't pass validation. It may contain the value phInvalidValue (defined
in UDialog), or the resource ID of any other ALRT resource. It would be easy to
override the routines involved to display a MacApp dialog box rather than a Toolbox
alert, in which case fAlertID could be the ID of the appropriate view resource.
THE NATURE OF VALIDITY
The TValidText declaration introduces a new method, IsValid:
FUNCTION TValidText.IsValid(
VAR theText: Str255;
VAR whyNot: INTEGER)
:BOOLEAN;
In addition to returning a Boolean indicating the validity of the given string, IsValid
returns in whyNot an indication of why the string is invalid (or the value noErr, if
it's valid). This is very similar in functionality to TEditText's Validate routine, with
one major difference: the string being validated is passed in as an argument. Where
TEditText.Validate assumes that it's supposed to validate the string currently being
edited, TValidText.IsValid can be used to test arbitrary strings for validity.
I overrode the Validate method in TValidText to make it a flow-of-control method. It
validates the current string and displays the error alert when necessary, as follows:
FUNCTION TValidText.Validate:LONGINT; OVERRIDE;
VAR
parentResult: LONGINT;
theText: Str255;
whyNot: INTEGER;
BEGIN
{Make sure the current text passes the superclass's validation.}
parentResult := INHERITED Validate;
IF (parentResult <> kValidValue)
THEN
Validate := parentResult
ELSE
BEGIN
GetText(theText);
IF IsValid(theText, whyNot)
THEN
Validate := HandleValidText(theText)
ELSE
Validate := HandleInvalidText(theText, whyNot);
END; {else}
END; {Validate}
This structure places the responsibility for handing invalid cases in the class itself,
rather than relying on MacApp's code for mapping error codes to error strings in
TDialogView.CantDeselect (never trust a method with a contraction in its name). Given
this structure, you can change any step in the validation process without changing the
nature of validation itself by overriding IsValid, HandleValidText, or
HandleInvalidText. That's the whole idea behind flow-of-control methods.
HandleValidText simply returns kValidValue (defined in UDialog), after notifying its
superview that the text is valid. Two lines of code--no fuss, no muss.
HandleInvalidText has to do a little more, but not much. It calls the method
ValidationErrorAlert to notify the user of the problem. Although the default alert has
only an OK button, I've also added support for a Cancel button. If the user clicks OK,
HandleAlertAccepted is called; otherwise--if the user clicks Cancel--
HandleAlertCancelled is called.
FUNCTION TValidText.HandleInvalidText(
VAR theText: Str255;
theError: INTEGER)
:LONGINT;
BEGIN
IF ValidationErrorAlert(theText, theError)
THEN
HandleInvalidText :=
HandleAlertAccepted(theText, theError)
ELSE
HandleInvalidText :=
HandleAlertCancelled(theText, theError);
In either case, a handler routine is called. Again, this kind of flow-of-control method,
which calls other methods to do the dirty work, is a very useful addition to the object
programmer's repertoire.
ValidationErrorAlert is equally trivial, consisting of only two lines. The first is a call
to PrepareErrorAlert, while the second displays the alert itself, returning TRUE if the
user accepts the dialog box and FALSE if the user cancels out of it.
PrepareErrorAlert is also only two lines of code:
PROCEDURE TValidText.PrepareErrorAlert(
VAR theText: Str255;
theError: INTEGER);
{This routine sets up the dialog that is displayed by
ValidationErrorAlert.}
VAR
theString: Str255;