Validation and user input handling
Validating and Validated events, ErrorProvider, masked and numeric input, and how to keep a dialog open when the data is wrong.
Validating and Validated
The Validating event fires when focus tries to leave a control whose CausesValidation is true. Setting e.Cancel = true blocks the move, which is exactly the behaviour you want for a field the user must fix.
var errors = new ErrorProvider { BlinkStyle = ErrorBlinkStyle.NeverBlink };
void EmailBox_Validating(object sender, CancelEventArgs e)
{
var text = emailBox.Text.Trim();
if (text.Length == 0)
{
errors.SetError(emailBox, "Email is required.");
e.Cancel = true; // focus stays here
return;
}
if (!text.Contains('@'))
{
errors.SetError(emailBox, "Enter a valid email address.");
e.Cancel = true;
return;
}
errors.SetError(emailBox, string.Empty); // clear the icon
}
void EmailBox_Validated(object sender, EventArgs e) => _draft.Email = emailBox.Text.Trim();- Use
Validatingto reject andValidatedto commit - keeping the two concerns separate avoids half-saved state. ErrorProviderneeds a container: passthisor the form of the control so the icon is positioned relative to it.- Cancel buttons must have
CausesValidation = false, otherwise the user cannot escape a form with an invalid field. - Never call
ValidateChildren()from inside aValidatinghandler - it recurses.
Choosing input controls
| Input | Control | Notes |
|---|---|---|
| Free text | TextBox | Set MaxLength; use CharacterCasing for codes |
| Fixed-format text | MaskedTextBox | A mask is a UI constraint, not validation - still validate the parsed value |
| Whole numbers | NumericUpDown | Set Minimum, Maximum and DecimalPlaces; it clamps rather than rejects |
| Money | NumericUpDown | decimal is the correct type; never store currency in a double |
| One of many | ComboBox | DropDownStyle = DropDownList prevents free text |
| Date | DateTimePicker | Set ShowCheckBox when the value is optional |
| Yes or no | CheckBox | Use CheckState.Indeterminate for a tri-state only when it is genuinely meaningful |
// a mask is not validation: "00/00/0000" still accepts 99/99/9999
var mask = new MaskedTextBox("00/00/0000") { InsertKeyMode = InsertKeyMode.Overwrite };
mask.Validating += (s, e) =>
{
if (!DateTime.TryParseExact(mask.Text, "dd/MM/yyyy",
CultureInfo.InvariantCulture, DateTimeStyles.None, out _))
{
errors.SetError(mask, "Enter a real date.");
e.Cancel = true;
}
};
amount.Maximum = 1_000_000m;
amount.DecimalPlaces = 2;
amount.ThousandsSeparator = true;Validating a whole form
private void OnSave(object sender, EventArgs e)
{
var ok = ValidateChildren(ValidationConstraints.Enabled | ValidationConstraints.Visible);
if (!ok)
{
DialogResult = DialogResult.None; // keep the dialog open
return;
}
Commit();
DialogResult = DialogResult.OK;
}
// disable implicit validation while a background load populates the form
private void BeginLoad()
{
AutoValidate = AutoValidate.Disable;
}
private void EndLoad()
{
AutoValidate = AutoValidate.EnableAllowFocusChange;
}⚠️
AutoValidate.EnablePreventFocusChange is the default and it will trap the user in a single control if a validation rule can never be satisfied - for example, a value that only becomes valid after a later field is filled. Prefer EnableAllowFocusChange plus a disabled Save button, and always give the user a way out.FAQ
Validating on every keystroke?
Not for blocking rules - that fights the user mid-typing. Validate on leave for format, and use a positive confirmation such as a green tick for live feedback that never blocks input.
Where should business rules live?
In a plain class you can unit test, not in the event handler. The handler should call the rule and translate the failure into an
ErrorProvider message - one rule, one place.Related
Dialogs, multiple forms and MDI DataGridView in depth
Last refreshed 2026-09-18.