Skip to main content
Published / updated

Server Controls and Event Handling

Before you start

You need: the page lifecycle (Article 02).

Time: about 40 minutes, plus the practice.

Learning objective

Find the handler for any control on a Web Forms page, including handlers that no name search will locate, and explain when each event fires.

Topics

  • HTML controls versus Web server controls
  • The controls you will meet most
  • Event wiring: Handles, AutoEventWireup, AddHandler
  • AutoPostBack and when a control causes a round trip
  • sender, EventArgs, and command events
  • Control ids, naming containers, and ClientID
  • FindControl

Two families of server control

<!-- HTML server control — a plain HTML element with runat="server" -->
<input type="text" id="txtCode" runat="server" />

<!-- Web server control — an ASP.NET object that renders its own HTML -->
<asp:TextBox ID="txtCode" runat="server" MaxLength="20" />
HTML server controlWeb server control
Syntax<input runat="server"><asp:TextBox>
Value property.Value.Text
Rendered outputWhat you wroteGenerated
Rich behaviourNoYes — validation, binding, events
Browser adaptationNoYes

Legacy applications mix both. The property name is the practical difference: reading .Text on an HTML control does not compile, and reading .Value on a TextBox does not either. When a property is not found, check which family the control belongs to.

Controls you will meet

<asp:Label ID="lblStatus" runat="server" Text="" />
<asp:TextBox ID="txtSearch" runat="server" MaxLength="50" />
<asp:Button ID="btnSearch" runat="server" Text="Search" />
<asp:LinkButton ID="lnkClear" runat="server" Text="Clear" />
<asp:DropDownList ID="ddlClass" runat="server" AutoPostBack="true" />
<asp:CheckBox ID="chkActive" runat="server" Text="Active only" />
<asp:RadioButtonList ID="rblSection" runat="server" RepeatDirection="Horizontal" />
<asp:HiddenField ID="hdnStudentId" runat="server" />
<asp:Panel ID="pnlResults" runat="server" Visible="false" />
<asp:Literal ID="litMessage" runat="server" Mode="Encode" />
<asp:GridView ID="grdStudents" runat="server" AutoGenerateColumns="False" />

Two of these carry a security note.

Label versus Literal. A Label renders a <span> wrapper; a Literal renders nothing but its content. Literal defaults to Mode="PassThrough", which emits raw HTML — assigning user input to it is a cross-site scripting hole. Set Mode="Encode" explicitly, or use a Label, which encodes by default when you assign to .Text.

HiddenField. The value goes to the browser and comes back. It is not secret and it is not trustworthy — a user can change it. Never store a SchoolId or a price there and use it without re-checking server-side.

Button versus LinkButton. A Button renders <input type="submit"> and posts normally. A LinkButton renders an <a> with a JavaScript __doPostBack call — so it does nothing when scripting is disabled, and it cannot be opened in a new tab.

Event wiring — three ways

This is where time is lost on legacy pages, because all three appear in the same application.

1. Handles clause

Protected WithEvents btnSearch As Button

Protected Sub btnSearch_Click(sender As Object, e As EventArgs) Handles btnSearch.Click
LoadStudents(txtSearch.Text)
End Sub

The field must be declared WithEvents — the designer file does this for you. Handles binds by the field name. Renaming the method is safe; renaming the control breaks every Handles clause pointing at it.

One handler can serve several events:

Protected Sub Filter_Changed(sender As Object, e As EventArgs) _
Handles ddlClass.SelectedIndexChanged, ddlSection.SelectedIndexChanged, chkActive.CheckedChanged

LoadStudents()
End Sub

2. AutoEventWireup

<%@ Page AutoEventWireup="true" ... %>
// No Handles clause, no AddHandler — wired purely by method name
protected void Page_Load(object sender, EventArgs e) { }

Only page-level events (Page_Load, Page_Init, Page_PreRender) are wired this way, and only when AutoEventWireup="true". C# projects usually have it on; VB projects usually use Handles.

The failure mode: with AutoEventWireup="false" and no Handles Me.Load, a method named Page_Load simply never runs. There is no error. The page loads blank and everything looks correct in the code.

3. AddHandler in markup or code

<asp:Button ID="btnSave" runat="server" OnClick="btnSave_Click" Text="Save" />
AddHandler btnApprove.Click, AddressOf Approve_Click

The markup OnClick attribute is the C# convention and appears in VB projects too. The code form is used for dynamic controls.

This is the one that costs you time. A handler wired with AddHandler has no Handles clause, so searching the code-behind for the control name finds only the declaration. When you cannot find what responds to a control:

Find in Files (Ctrl+Shift+F), Look in: Entire Solution:

Search forFile typesFinds
AddHandler*.vb;*.csHandlers wired in code rather than by Handles
AddressOf*.vbThe method being wired up
OnClick=*.aspx;*.ascxHandlers wired in the markup instead

Check the markup as well as the code-behind. A button can be wired three ways — a Handles clause, an OnClick attribute in the .aspx, or an AddHandler call at run time — and a control that appears to do nothing usually has its handler in whichever place you did not look.

AutoPostBack

Most controls do not cause a postback when the user changes them. Their event fires later, batched into the next postback.

<!-- No AutoPostBack — SelectedIndexChanged fires on the NEXT postback -->
<asp:DropDownList ID="ddlClass" runat="server" />

<!-- AutoPostBack — changing it immediately posts the page -->
<asp:DropDownList ID="ddlClass" runat="server" AutoPostBack="true" />

Without AutoPostBack, selecting a class does nothing visible. The user selects, then clicks Search, and then both SelectedIndexChanged and btnSearch_Click fire in the same request — change events run before the click event.

That ordering matters. A dependent dropdown ("choose class, then section fills") needs AutoPostBack="true" on the first one, or the second never populates until something else posts.

AutoPostBack renders an onchange attribute calling __doPostBack. Every change is a full page round trip, so on a form with six auto-posting controls the page feels slow — a real complaint on legacy screens, and the reason UpdatePanel exists.

sender and EventArgs

Protected Sub Approve_Click(sender As Object, e As EventArgs)
Dim button As Button = DirectCast(sender, Button)
Dim studentId As Integer = CInt(button.CommandArgument)

ApproveStudent(studentId)
End Sub

sender is the control that raised the event, which lets one handler serve many controls. Use DirectCast when you are certain of the type and TryCast when you are not.

Command events carry more:

<asp:Button ID="btnRow" runat="server"
CommandName="Approve"
CommandArgument='<%# Eval("Id") %>'
Text="Approve" />
Protected Sub Row_Command(sender As Object, e As CommandEventArgs)
Select Case e.CommandName
Case "Approve"
ApproveStudent(CInt(e.CommandArgument))
Case "Reject"
RejectStudent(CInt(e.CommandArgument))
End Select
End Sub

CommandName and CommandArgument are how buttons inside a GridView row identify which row was clicked. CommandArgument arrives as a string from the browser and is user-modifiable — validate it, and re-check that the row belongs to the current user's school before acting on it.

Control ids and naming containers

A control's rendered id is not the id you wrote. Master pages, user controls, and data-bound rows are naming containers, and each one prefixes the ids inside it.

<!-- Written -->
<asp:TextBox ID="txtSearch" runat="server" />

<!-- Rendered inside a master page -->
<input name="ctl00$MainContent$txtSearch" id="MainContent_txtSearch" type="text" />

<!-- Rendered inside GridView row 3 -->
<input name="ctl00$MainContent$grdStudents$ctl05$txtMarks"
id="MainContent_grdStudents_ctl05_txtMarks" type="text" />

Never hard-code a rendered id in JavaScript or CSS. Use ClientID:

var box = document.getElementById('<%= txtSearch.ClientID %>');

Or make the id predictable:

<asp:TextBox ID="txtSearch" runat="server" ClientIDMode="Static" />

ClientIDMode="Static" keeps exactly the id you wrote — but do not use it inside a repeated row, because every row would then share one id.

When a JavaScript selector stops working after a page is moved into a master page, this is why.

FindControl

To reach a control inside a naming container from code:

Protected Sub grdStudents_RowDataBound(sender As Object, e As GridViewRowEventArgs) _
Handles grdStudents.RowDataBound

If e.Row.RowType <> DataControlRowType.DataRow Then
Return
End If

Dim marksBox As TextBox = TryCast(e.Row.FindControl("txtMarks"), TextBox)

If marksBox Is Nothing Then
Return
End If

marksBox.Text = DataBinder.Eval(e.Row.DataItem, "MarksObtained").ToString()
End Sub

FindControl searches one level within the given container. It does not recurse. Calling Page.FindControl("txtMarks") for a control inside a grid row returns Nothing, which is the usual cause of a null-reference in RowDataBound.

Always TryCast and test for Nothing. FindControl returns Nothing for a wrong name with no error, and the resulting crash points at the line after, not at the typo.

Errors you will hit

What you seeCauseFix
Button does nothingHandler wired three possible ways — check all threeHandles, OnClick= in markup, or AddHandler
Handler runs twiceWired in markup and with Handles or AddHandlerRemove one
Control 'x' of type 'y' must be placed inside a form tag with runat=serverControl outside the <form runat="server">Move it inside
txtName is Nothing in code-behindThe .designer file is staleOpen in Design view to regenerate
FindControl returns Nothing for a control inside a GridViewIt is in a naming containerSearch from the row, not the page
A TextBox change event never firesAutoPostBack is FalseSet AutoPostBack="True", or wait for the next postback

A control that appears dead usually has its handler somewhere you did not look. Search the markup, the code-behind and for AddHandler before concluding it is unwired.

Common mistakes

  • Reading .Text on an HTML server control, or .Value on a Web server control
  • Assigning user input to a Literal in PassThrough mode — an XSS hole
  • Trusting a HiddenField value without re-checking it server-side
  • Assuming a control with no Handles clause has no handler
  • AutoEventWireup="false" with no Handles Me.Load, so Page_Load silently never runs
  • Expecting SelectedIndexChanged to fire without AutoPostBack="true"
  • Hard-coding a rendered id in JavaScript
  • ClientIDMode="Static" on a control inside a repeated row
  • Using FindControl from the wrong container and getting Nothing
  • Trusting CommandArgument without validating it

Practice

On an existing Web Forms page, inventory every server control: its type, how its event is wired, and whether AutoPostBack is set. Find at least one handler wired by something other than Handles.

Then work the course debugging exercise — find why an event does not fire. Reproduce all four causes and record the symptom of each:

  1. Remove runat="server" from a button.
  2. Set AutoEventWireup="false" and delete the Handles Me.Load clause.
  3. Remove AutoPostBack="true" from a dropdown a dependent control relies on.
  4. Rename a control field that has a Handles clause pointing at it.

Finally, view source on a page inside a master page and compare a written control id with its rendered id and name attributes.

You can now

  • Find the handler for any control, however it was wired
  • Predict whether a control causes a postback
  • Use AutoPostBack deliberately rather than by accident
  • Reach controls inside a naming container safely
  • Search a solution with Find in Files to map event wiring

Review questions

  1. What are the three ways a Web Forms event handler can be wired, and which is hardest to find?
  2. What happens with AutoEventWireup="false" and no Handles Me.Load?
  3. Why does a dropdown's SelectedIndexChanged sometimes not fire until the user clicks a button?
  4. Why does Page.FindControl return Nothing for a control inside a GridView row?

Next: State management