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.IsValidand why the control is not enoughValidationSummaryand error display- Validation groups
CausesValidationand buttons that must skip itCustomValidatorfor 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" />
| Validator | Checks |
|---|---|
RequiredFieldValidator | The control is not empty |
RangeValidator | Value falls between a minimum and maximum |
CompareValidator | Compares to a value, another control, or a data type |
RegularExpressionValidator | Matches a pattern |
CustomValidator | Your own rule, client and/or server |
ValidationSummary | Displays 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:
Textshows next to the field — usually*.ErrorMessageshows in theValidationSummary. Setting onlyErrorMessageprints the full message next to the field as well, which usually wrecks the layout.Display="Dynamic"takes no space when valid.Staticreserves the space, which keeps the layout from jumping.Noneshows nothing inline and relies entirely on the summary.RequiredFieldValidatoris separate. Every other validator passes on an empty value. ARangeValidatoralone 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 aRequiredFieldValidator. ControlToValidatecan be omitted for a rule spanning several fields.args.Valueis then empty and you read the controls directly — andValidateEmptyText="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
| Symptom | Cause |
|---|---|
| Submit succeeds with an empty field | Only a Range or RegularExpression validator — no RequiredFieldValidator |
| Validation shows but the save still happens | Missing If Not Page.IsValid in the handler |
| One button ignores its validators | CausesValidation="false", or a ValidationGroup mismatch |
| Cancel button does nothing | CausesValidation not set to false |
| Validators never appear at all | Control is inside an UpdatePanel without script registration, or scripts are 404ing |
| Message appears twice | ErrorMessage set with no Text, plus a ValidationSummary |
| Layout jumps when an error appears | Display="Static" needed instead of Dynamic |
HttpException on Page.IsValid | Read 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 see | Cause | Fix |
|---|---|---|
| Validator shows the message but the form still submits | Server code did not check Page.IsValid | Check it in the handler, first line |
| Validation passes with scripting disabled | Relied on the client script only | The server check is the control; the client one is a courtesy |
| A validator blocks an unrelated button | No ValidationGroup set | Group the validators and the button together |
RequiredFieldValidator does not fire on a dropdown | InitialValue not set | Set it to the placeholder's value |
CompareValidator always fails on a date | Culture mismatch in parsing | Set the type and the culture explicitly |
| Cancel button triggers validation | CausesValidation defaults to True | Set 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.IsValidin the save handler - Using
RangeValidatoralone and expecting the field to be mandatory - Forgetting
ValidationGroupon the summary or one validator - No
CausesValidation="false"on Cancel CustomValidatoron an optional field withoutValidateEmptyText="true"- Setting
ErrorMessagewithoutText, 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:
- Submit with everything empty — confirm the summary lists each error.
- Disable JavaScript in the browser and submit invalid data. Confirm the page still refuses to save, and identify which line stopped it.
- Remove
If Not Page.IsValidand repeat step 2. Confirm invalid data now reaches the database. - 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.IsValidin every handler that saves - Use
ValidationGroupso 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
- Why is client-side validation not sufficient, and what makes server-side validation effective?
- Why does a
RangeValidatoralone fail to make a field mandatory? - What happens to a Cancel button without
CausesValidation="false"? - Why keep both a
CustomValidatorduplicate check and a database unique constraint?
Next: GridView and CRUD