Skip to main content
Published / updated

Forms and Validation Controls

Before you start

You need: server controls (Article 03) and the lifecycle (Article 02).

Time: about 40 minutes, plus the practice.

Learning objective

Build a form whose validation cannot be bypassed, and diagnose a validator that fails to block a submit.

Topics

  • The validator control family
  • Page.IsValid and why the control is not enough
  • ValidationSummary and error display
  • Validation groups
  • CausesValidation and buttons that must skip it
  • CustomValidator for rules the built-ins cannot express
  • Why client-side validation is never the control
  • Diagnosing validators that do not fire

The validator family

Each validator watches one control and reports whether it is valid.

<asp:TextBox ID="txtName" runat="server" MaxLength="100" />
<asp:RequiredFieldValidator ID="rfvName" runat="server"
ControlToValidate="txtName"
ErrorMessage="Student name is required."
Text="*"
Display="Dynamic"
CssClass="text-danger" />
ValidatorChecks
RequiredFieldValidatorThe control is not empty
RangeValidatorValue falls between a minimum and maximum
CompareValidatorCompares to a value, another control, or a data type
RegularExpressionValidatorMatches a pattern
CustomValidatorYour own rule, client and/or server
ValidationSummaryDisplays all messages together

A complete form:

<asp:TextBox ID="txtRollNumber" runat="server" MaxLength="20" />
<asp:RequiredFieldValidator runat="server"
ControlToValidate="txtRollNumber"
ErrorMessage="Roll number is required."
Text="*" Display="Dynamic" ValidationGroup="StudentForm" />
<asp:RegularExpressionValidator runat="server"
ControlToValidate="txtRollNumber"
ValidationExpression="^NCA-\d{4}-\d{4}$"
ErrorMessage="Roll number must look like NCA-2024-0012."
Text="*" Display="Dynamic" ValidationGroup="StudentForm" />

<asp:TextBox ID="txtMarks" runat="server" />
<asp:RangeValidator runat="server"
ControlToValidate="txtMarks"
MinimumValue="0" MaximumValue="100" Type="Integer"
ErrorMessage="Marks must be between 0 and 100."
Text="*" Display="Dynamic" ValidationGroup="StudentForm" />

<asp:TextBox ID="txtParentPhone" runat="server" MaxLength="10" />
<asp:RegularExpressionValidator runat="server"
ControlToValidate="txtParentPhone"
ValidationExpression="^[6-9]\d{9}$"
ErrorMessage="Enter a valid 10-digit mobile number."
Text="*" Display="Dynamic" ValidationGroup="StudentForm" />

Three attributes worth understanding:

  • Text shows next to the field — usually *. ErrorMessage shows in the ValidationSummary. Setting only ErrorMessage prints the full message next to the field as well, which usually wrecks the layout.
  • Display="Dynamic" takes no space when valid. Static reserves the space, which keeps the layout from jumping. None shows nothing inline and relies entirely on the summary.
  • RequiredFieldValidator is separate. Every other validator passes on an empty value. A RangeValidator alone does not make a field mandatory — this is the single most common validator mistake.

Page.IsValid — the check that actually matters

Protected Sub btnSave_Click(sender As Object, e As EventArgs) Handles btnSave.Click
If Not Page.IsValid Then
Return
End If

SaveStudent()
End Sub

Client-side validation blocks the submit in a browser with scripting enabled. That is a convenience, not a control. It is bypassed by disabling JavaScript, by DevTools, or by posting directly with a tool — none of which is exotic.

Server-side validation runs on every postback regardless, and Page.IsValid reports the result. Without that check, the handler runs on invalid data and saves it.

If you call Page.Validate() yourself — for a specific group, or after changing a validator — Page.IsValid is only meaningful after that call.

Page.Validate("StudentForm")

If Not Page.IsValid Then
Return
End If

Reading Page.IsValid before any validation has occurred throws HttpException.

ValidationSummary

<asp:ValidationSummary ID="vsStudent" runat="server"
ValidationGroup="StudentForm"
CssClass="alert alert-danger"
HeaderText="Please correct the following:"
DisplayMode="BulletList"
ShowSummary="true"
ShowMessageBox="false" />

ShowMessageBox="true" produces a JavaScript alert. It appears in older applications and is worth removing — it is disruptive, unstyleable, and does nothing without scripting.

Place the summary where the user will see it, at the top of the form. A summary below a long form scrolls out of view and users report that "nothing happens" when they submit.

Validation groups

A page with two independent forms needs them separated, or the search box's required-field validator blocks the save button.

<!-- Search area -->
<asp:TextBox ID="txtSearch" runat="server" />
<asp:RequiredFieldValidator runat="server" ControlToValidate="txtSearch"
ErrorMessage="Enter a search term." Text="*" ValidationGroup="Search" />
<asp:Button ID="btnSearch" runat="server" Text="Search" ValidationGroup="Search" />

<!-- Student form -->
<asp:TextBox ID="txtName" runat="server" />
<asp:RequiredFieldValidator runat="server" ControlToValidate="txtName"
ErrorMessage="Name is required." Text="*" ValidationGroup="StudentForm" />
<asp:Button ID="btnSave" runat="server" Text="Save" ValidationGroup="StudentForm" />

Every validator, every button, and the summary must carry the same ValidationGroup string. A validator left out of the group never fires; a button in the wrong group triggers the wrong validators.

Validators with no ValidationGroup belong to the default group, and a button with no group triggers only those. Mixing grouped and ungrouped validators on one page is a reliable source of "the required field doesn't work".

CausesValidation

Some buttons must submit without validating.

<asp:Button ID="btnCancel" runat="server" Text="Cancel" CausesValidation="false" />
<asp:Button ID="btnAddRow" runat="server" Text="Add row" CausesValidation="false" />

Cancel, Back, and any button that only rearranges the form should set CausesValidation="false". Without it, a user who half-filled a form cannot cancel out of it — the validators block the postback and the page appears frozen. This is a common and infuriating legacy defect.

ImageButton, LinkButton, and GridView command buttons all have the same property.

CustomValidator

For rules the built-ins cannot express — anything needing the database, or two fields compared.

<asp:TextBox ID="txtRollNumber" runat="server" />
<asp:CustomValidator ID="cvRollNumber" runat="server"
ControlToValidate="txtRollNumber"
OnServerValidate="cvRollNumber_ServerValidate"
ErrorMessage="This roll number is already used by another student."
Text="*" Display="Dynamic" ValidationGroup="StudentForm" />
Protected Sub cvRollNumber_ServerValidate(source As Object, args As ServerValidateEventArgs)
Dim schoolId As Integer = CInt(Session("SchoolId"))

args.IsValid = Not _repository.RollNumberExists(schoolId, args.Value, _editingPublicId)
End Sub

Two things about CustomValidator:

  • It does not fire on an empty value by default. Set ValidateEmptyText="true" if the rule must apply to blanks, or pair it with a RequiredFieldValidator.
  • ControlToValidate can be omitted for a rule spanning several fields. args.Value is then empty and you read the controls directly — and ValidateEmptyText="true" becomes mandatory, or it never runs at all.
Protected Sub cvDates_ServerValidate(source As Object, args As ServerValidateEventArgs)
Dim fromDate As Date
Dim toDate As Date

If Not Date.TryParse(txtFrom.Text, fromDate) OrElse Not Date.TryParse(txtTo.Text, toDate) Then
args.IsValid = False
Return
End If

args.IsValid = fromDate <= toDate
End Sub

Rules validators cannot cover

Validator controls check the shape of input. They cannot enforce anything requiring the current user, the database at save time, or business context.

Protected Sub btnSave_Click(sender As Object, e As EventArgs) Handles btnSave.Click
If Not Page.IsValid Then
Return
End If

Dim schoolId As Integer = CInt(Session("SchoolId"))

' Authorisation — never a validator's job
If Not UserCanEditStudents() Then
Response.Redirect("~/AccessDenied.aspx", False)
Return
End If

' The record must belong to this school, whatever the query string says
Dim student As Student = _repository.GetByPublicId(schoolId, _editingPublicId)

If student Is Nothing Then
lblError.Text = "Student not found."
Return
End If

Try
_repository.Update(student)
Response.Redirect("~/Students/List.aspx?saved=1", False)
Return

Catch ex As SqlException When ex.Number = 2627
' The database constraint is the real guarantee
cvRollNumber.IsValid = False
lblError.Text = "This roll number was just taken by another user."
End Try
End Sub

The duplicate check appears twice on purpose. The CustomValidator gives a friendly message; the unique constraint catches the case where two users submit the same roll number a moment apart. Only the database can win that race.

Diagnosing validators that do not fire

SymptomCause
Submit succeeds with an empty fieldOnly a Range or RegularExpression validator — no RequiredFieldValidator
Validation shows but the save still happensMissing If Not Page.IsValid in the handler
One button ignores its validatorsCausesValidation="false", or a ValidationGroup mismatch
Cancel button does nothingCausesValidation not set to false
Validators never appear at allControl is inside an UpdatePanel without script registration, or scripts are 404ing
Message appears twiceErrorMessage set with no Text, plus a ValidationSummary
Layout jumps when an error appearsDisplay="Static" needed instead of Dynamic
HttpException on Page.IsValidRead before Validate() ran

For the "validators never appear" case, check the browser console. Web Forms validation depends on WebResource.axd and ScriptResource.axd. A misconfigured machineKey, an aggressive URL filter, or a missing handler registration makes those 404, and all client validation silently stops. Server-side validation still runs — which is exactly why Page.IsValid must be checked.

Errors you will hit

What you seeCauseFix
Validator shows the message but the form still submitsServer code did not check Page.IsValidCheck it in the handler, first line
Validation passes with scripting disabledRelied on the client script onlyThe server check is the control; the client one is a courtesy
A validator blocks an unrelated buttonNo ValidationGroup setGroup the validators and the button together
RequiredFieldValidator does not fire on a dropdownInitialValue not setSet it to the placeholder's value
CompareValidator always fails on a dateCulture mismatch in parsingSet the type and the culture explicitly
Cancel button triggers validationCausesValidation defaults to TrueSet CausesValidation="False"

Page.IsValid is not checked for you. The validators run, the message appears, and unless your handler tests Page.IsValid the save goes ahead anyway.

Common mistakes

  • Relying on client-side validation as the control
  • Omitting If Not Page.IsValid in the save handler
  • Using RangeValidator alone and expecting the field to be mandatory
  • Forgetting ValidationGroup on the summary or one validator
  • No CausesValidation="false" on Cancel
  • CustomValidator on an optional field without ValidateEmptyText="true"
  • Setting ErrorMessage without Text, printing the full message inline
  • ShowMessageBox="true" alerts
  • A duplicate check in code with no matching database constraint
  • Trusting a query-string id at save time

Practice

Build the course exercise — a validated form. Create a student form with name, roll number, class, section, date of birth, and parent phone. Use RequiredFieldValidator on every mandatory field, a RegularExpressionValidator for the roll number and phone, a RangeValidator for marks, and a CustomValidator checking the roll number is unique. Add a ValidationSummary and put every control in one validation group. Add a Cancel button that works from a half-filled form.

Then prove the validation cannot be bypassed:

  1. Submit with everything empty — confirm the summary lists each error.
  2. Disable JavaScript in the browser and submit invalid data. Confirm the page still refuses to save, and identify which line stopped it.
  3. Remove If Not Page.IsValid and repeat step 2. Confirm invalid data now reaches the database.
  4. Put the request through browser DevTools, remove the validator's script, and submit. Same conclusion.

Step 3 is the one to remember: the validator controls did not fail — the handler simply did not ask.

You can now

  • Build a form whose validation holds with scripting disabled
  • Check Page.IsValid in every handler that saves
  • Use ValidationGroup so one form does not block another
  • Set CausesValidation="False" on Cancel
  • Diagnose from the symptom why a validator is not blocking a submit

Review questions

  1. Why is client-side validation not sufficient, and what makes server-side validation effective?
  2. Why does a RangeValidator alone fail to make a field mandatory?
  3. What happens to a Cancel button without CausesValidation="false"?
  4. Why keep both a CustomValidator duplicate check and a database unique constraint?

Next: GridView and CRUD