The Problem
The DDV mechanism is far too primitive to be useful. It only validates on the OK button (or, more specifically, on the UpdateData
that accompanies it), which delays the validation until far too late. It also issues error messages that are related to the representation in the program, not to the problem domain. I have thus avoided this toy mechanism and written my own validation code. I also find it particularly annoying that I am allowed to click the OK button when there is an error; this violates the basic principles of GUI design. So DDV is not only hard for the user to use, it actually violates the GUI design guidelines since normally you must click a button which is not actually valid to click before the DDV mechanism can be invoked!
Sure, it makes it easier to program. But the goal is not to make applications easy to program as much as it is to make applications easy to use. I believe my techniques contribute significantly to the latter. The fact that they are not easier to program is of little consequence.
This particular example is useful as it illustrates several useful techniques. These include:
- Showing how to validate input on a character-by-character basis
- Providing useful feedback immediately as to the correctness of the input
- Showing how to use a ToolTip to indicate the reason that the OK button is disabled.
I have used all of these techniques in some form or other in building very informative, user-oriented validation methods.
The Solutions
The first thing I include is a validating edit control. This particular control solves a problem many users ask for: an edit control that validates floating-point input. However, you can replace the FSM with one that validates dates, times, Social Security Numbers, or any other textual form you can parse. The validation does not have to be limited to simple parsing, although it is clear from the fact that every change initiates a validation that you do not want to do some sort of database lookup on every character. In such a case, you would more likely do the validation on the WM_KILLFOCUS
(OnKillFocus
) event. But that's a different problem than the one this control addresses.
The way I handle this is to handle the reflected WM_COMMAND
/EN_CHANGE
message. When the contents change, I read the entire string and reparse it. This is one of the methodological changes from traditional getch
-style input where you could simply run the FSM on each keystroke. In Windows, the keystrokes have nothing to do with the order of the content of the edit control, because the user can reposition the input caret anywhere in the string. So the entire string must be reparsed, from the start, each time.
Syntax Check
In this case, I parse the floating point number using (a subset of) the syntax specified for the atof
function. (I don't accept D or d as exponent indicators).
[whitespace] [sign] [digits] [.digits] [{ e | E} [sign] digits]
(Actually, I don't let the user type the whitespace in, but we'll talk about that below). The parsing is done via a Finite State Machine (FSM) which takes the current state and the current character and decodes against a table indicating the next state. The table is encoded as a sequence of case
statements inside a switch
. In each match, I can do one or more of the following:
- "Eat" the character, removing it from the input stream, or leaving it for the next state to process
- Set the next state
- Set an indicator as to whether the string is complete, incomplete, or erroneous.
This is encoded as shown (partially) below. To set the "indicator", I set a brush value to the pointer to a predefined brush. If the brush ever gets set to the "error" indicator, the loop ends.
int state = S0;
for(int i = 0; brush != &errorBrush && i < s.GetLength();)
{
TCHAR ch = s[i];
switch(MAKELONG(state, ch))
{
case MAKELONG(S0, _T(' ')):
case MAKELONG(S0, _T('\t')):
i++;
continue;
case MAKELONG(S0, _T('+')):
case MAKELONG(S0, _T('-')):
i++;
brush = &partialBrush;
state = IPART;
continue;
case MAKELONG(S0, _T('0')):
�
�
�
case MAKELONG(S0, _T('9')):
state = IPART;
continue;
case MAKELONG(S0, _T('.')):
i++;
state = FPART;
brush = &partialBrush;
continue;
case MAKELONG(S0, _T('E')):
case MAKELONG(S0, _T('e')):
i++;
state = ESIGN;
brush = &partialBrush;
continue;
case MAKELONG(IPART, _T('0')):
�
�
�
case MAKELONG(IPART, _T('9')):
i++;
brush = &OKBrush;
continue;
case MAKELONG(IPART, _T('.')):
i++;
brush = &OKBrush;
state = FPART;
continue;
case MAKELONG(IPART, _T('e')):
case MAKELONG(IPART, _T('E')):
i++;
brush = &partialBrush;
state = ESIGN;
continue;
case MAKELONG(FPART, _T('0')):
case MAKELONG(FPART, _T('9')):
i++;
brush = &OKBrush;
continue;
case MAKELONG(FPART, _T('e')):
case MAKELONG(FPART, _T('E')):
i++;
brush = &partialBrush;
state = ESIGN;
continue;
case MAKELONG(ESIGN, _T('+')):
case MAKELONG(ESIGN, _T('-')):
i++;
brush = &partialBrush;
state = EPART;
continue;
case MAKELONG(ESIGN, _T('0')):
case MAKELONG(ESIGN, _T('1')):
�
�
�
case MAKELONG(ESIGN, _T('9')):
state = EPART;
continue;
case MAKELONG(EPART, _T('0')):
�
�
�
case MAKELONG(EPART, _T('9')):
i++;
brush = &OKBrush;
continue;
default:
brush = &errorBrush;
continue;
}
}
To absorb a character, I just increment the pointer (i++
). You can create a similar table to parse a date, time, or any other field you can define.
Value Check
Values which are syntactically correct may not meet other criteria. For example, credit card numbers apply a validation algorithm in which one of the digits (usually the low-order one) is some function of the preceding digits. One common scheme years ago would add up the digits modulo 10, then subtract the resulting value from 9, and use the resulting digit as the low-order digit. You might put range checks in place, or validate that the day of the month does not exceed the valid range for the selected month (no February 31st, for example).
In my sample program, I limit the value to have an absolute value of greater than 1.0, a positive value of <= 8192.0f, and a negative value of >= -16384.0f. How do we couple the value range check into the basic validation? The answer is that any time I get a syntactically valid number, I send a message to the parent window requesting that it validate the control. It returns, from the SendMessage
, a Boolean value of TRUE
or FALSE
to indicate if the value is valid.
To do this, I use a user-defined message, in fact, a Registered Window Message, to notify the parent. See my essay on Message Management for more details about this. In this case, I use a static class member variable, which I declare in the class as:
static UINT UWM_CHECK_VALUE;
I initialize this in the .cpp file by doing:
UINT CFloatingEdit::UWM_VALID_CHANGE = ::RegisterWindowMessage(
_T("UWM_VALID_CHANGE-{6FE8A4C1-AE33-11d4-A002-006067718D04}"));
I react to this message by placing the following line the the MESSAGE_MAP
of the parent. Note that the message request follows the magic ClassWizard comments.
ON_REGISTERED_MESSAGE(CFloatingEdit::UWM_CHECK_VALUE, OnCheckValue)
I have defined the parameters of this message to be as shown:
Display Change
In order to indicate the state and provide immediate feedback to the user, I modify the background color of the control. I selected white for an empty control, red for an invalid value, yellow for a value that is syntactically correct so far but is not yet completely valid, and green for values that meet all criteria. These are illustrated below.
|
The edit control is empty. It displays as the normal edit background, which on this machine is white. |
|
The edit control has a syntactically valid value that matches any range constraints (for this example, range checks are not enabled). |
|
The edit control has a value that has not yet been completed. It is not yet syntactically valid, but what is there so far is correct. |
|
The edit control has a value that is not syntactically correct. No amount of addition to this value can make it correct. |
Control Update
I need to update the controls in response to changes in the validation. This means that I need to enable or disable controls (such as the OK button) whenever the state changes. In order to do this, I send a notification to the parent window indicating that a change in the validation status has occurred. This is another Registered Window Message.
When I get this message, I invoke the following handler:
LRESULT CValidatorDlg::OnValidChange(WPARAM, LPARAM lParam)
{
CWnd::FromHandle((HWND)lParam)->InvalidateRect(NULL);
updateControls();
return 0;
}
Note the initial InvalidateRect
. I have to force a redraw of the entire control when the valid state changes, else the display ends up slightly skewed, with only the background behind the letters being redrawn. As you read the code, you will find several other full-control invalidations that guarantee the correct appearance is maintained. These are necessary because Windows is actually very good at minimizing the redrawing of the control, and if you type the value as shown below without this invalidation, the results are strange indeed. You may even note that some of the values which, if typed left-to-right, should have produced valid values appear in red indicating invalid values. This is because the InvalidateRect
had not been done at all. Note also how parts of the background which are outside the character cells is invalid.
ToolTips
I found that when there are many controls on the dialog, and several of them affect whether or not a control (such as the OK button) is enabled, it is often quite informative to use a ToolTip to specify the text explaining why the control is not enabled. Often, there are several causes. In this case, the problem is which one to display. ToolTips have a limited string length they can display, and characters beyond this will not be displayed. My strategy is to adopt a mechanism that involves a rule-based system that examines the conditions and displays the first condition that has disabled the control. Sometimes I arrange these rules so that the most common, or easiest-to-fix, condition is the one displayed first.
To enable ToolTips, you must call the function EnableToolTips(TRUE)
in the OnInitDialog
handler. In addition, I do the ToolTip by using a callback function. This means I must add the following message handler to my MESSAGE_MAP
, outside the magic comments:
ON_NOTIFY_EX(TTN_NEEDTEXT, 0, OnToolTipNotify)
The handler is defined as follows:
BOOL CValidatorDlg::OnToolTipNotify(UINT, NMHDR * pNMHDR, LRESULT *)
{
TOOLTIPTEXT * pTTT = (TOOLTIPTEXT *)pNMHDR;
HWND ctl = (HWND)pNMHDR->idFrom;
UINT msg = 0;
if(pTTT->uFlags & TTF_IDISHWND)
{
UINT id = ::GetDlgCtrlID(ctl);
switch(id)
{
case IDC_SAMPLE:
Within each case for each control I put the rules, or a call on a function that computes the rules, for that control. For example, to make the messages clearer, I have two internal state functions in the CFloatingEdit
control, one of which tells me if the value is syntactically valid and one of which tells me if the value is semantically valid. The routine IsValid
returns TRUE
only if the value is both syntactically and semantically valid. This allows me to report in more detail why the value is not valid. For the semantic check, I call the same function that the child validity check used, which returns me a string ID for a string to display if there is an error, or 0 if there is not an error. Ultimately, this value is stored in the variable msg
. If this value is 0, there is nothing to display, and I return FALSE
from the handler; otherwise, I set up some fields in the structure passed in, and return TRUE
.
if(msg == 0)
return FALSE;
pTTT->lpszText = MAKEINTRESOURCE(msg);
pTTT->hinst = AfxGetResourceHandle();
return TRUE;
This causes the string designated by the string ID stored in msg
to be displayed as the ToolTip.
In a real application, the text would have been more informative, for example, "Temperature value is not in correct format", but the generalization to that should now be obvious.
Input Limitation
To reduce the chances of error even further, I lock out characters that are not actually valid. So for my floating-point control, I disallow all characters except the digits, plus and minus signs, decimal point, and the letters 'e' and 'E'. And backspace. Don't forget backspace!
A common piece of advice that appears is to "put this in the PreTranslateMessage
handler of your dialog". This doesn't make any sense to me; it violates any number of issues of abstraction and object orientation. It makes a lot more sense to me to put this in the control that wants to filter the characters. To do this, I write a handler like the one shown below, which appears in my subclassed dialog. This gets created when I add a WM_CHAR
handler using ClassWizard, and all I do is fill in the code shown.
void CFloatingEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
switch(nChar)
{
case _T('+'):
case _T('-'):
case _T('.'):
case _T('E'):
case _T('e'):
case _T('0'):
case _T('1'):
case _T('2'):
case _T('3'):
case _T('4'):
case _T('5'):
case _T('6'):
case _T('7'):
case _T('8'):
case _T('9'):
case _T('\b'):
break;
default:
MessageBeep(0);
return;
}
CEdit::OnChar(nChar, nRepCnt, nFlags);
}
All this does is accept the characters shown, and call the superclass handler, or simply issue a beep and return, thus discarding the character.
Summary
This article summarizes a set of techniques I use extensively in the applications I build. I've not seen this set of ideas documented elsewhere, so this seemed a good topic for an essay.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.
Send mail to newcomer@flounder.com with questions or comments about this article.
Copyright � 1999 The Joseph M. Newcomer Co. All Rights Reserved.