Skip to main content
Published / updated

The Page Lifecycle and Postbacks

Before you start

You need: the project layout and runat="server" (Article 01).

Time: about 40 minutes, plus the practice. The lifecycle order is worth memorising — most Web Forms bugs are a lifecycle misunderstanding.

Learning objective

Given a Web Forms page that misbehaves after a button click, name the lifecycle stage responsible and explain why.

Topics

  • What a postback is
  • The lifecycle event order
  • IsPostBack and why it is the most important check on the page
  • When ViewState loads, and when control values are available
  • Where to put which kind of code
  • Dynamic controls and why they lose their handlers
  • Tracing the lifecycle

Terminology

TermMeaning
PostbackA POST request a page sends to itself, carrying its own state
Initial requestThe first GET of a page — no ViewState, no posted values
Round tripOne full request/response cycle to the server
LifecycleThe fixed sequence of events ASP.NET raises while building a page

What a postback is

Every server control that causes an action submits the page's single form back to the same URL. The server then rebuilds the entire page object from scratch, restores its state, runs your event handler, re-renders the whole thing, and sends it back.

Initial GET → Page object created → Page_Load → render → HTML
Button click → POST to same URL → Page object created AGAIN
→ ViewState restored
→ Page_Load
→ btnSearch_Click
→ render → HTML

Nothing survives between requests except what is deliberately stored — ViewState, Session, or the database. The page object itself is destroyed and recreated on every single request.

That last sentence explains more Web Forms bugs than anything else. A field you set in Page_Load is gone by the next click unless something persisted it.

The lifecycle order

#EventWhat is availableWhat belongs here
1PreInitNothing restoredSet master page, theme, create dynamic controls
2InitControls exist, no ViewStateWire dynamic handlers
3InitComplete
4ViewState loadedControl properties restored(framework)
5Postback data appliedPosted values in controls(framework)
6PreLoadEverything restoredRare
7Load (Page_Load)Full state and posted valuesMost of your code
8Control eventsbtnSearch_Click, SelectedIndexChanged
9LoadCompleteAll events doneWork depending on every handler
10PreRenderFinal chance to change outputLate data binding
11ViewState saved(framework)
12Render(framework)
13UnloadPage being discardedCleanup — cannot change output

Two orderings decide almost everything:

  • ViewState and posted values are restored before Page_Load. Reading txtSearch.Text in Page_Load on a postback gives you what the user typed.
  • Page_Load runs before the click handler. Always. On every postback.

IsPostBack

The second ordering is why this check exists, and why omitting it is the defining Web Forms defect.

Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
If Not IsPostBack Then
LoadClassDropDown()
LoadStudents()
End If
End Sub

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

Remove the If Not IsPostBack and follow what happens when the user clicks Search:

1. Page_Load runs → LoadStudents() with no filter → grid shows ALL students
2. btnSearch_Click → LoadStudents(term) → grid shows filtered students
3. ... but LoadClassDropDown() also re-ran in step 1,
resetting the dropdown and discarding the user's selection

Depending on binding order the symptoms differ, but they are always one of these three, and all three are reported by users as something else:

Reported asActual cause
"The filter does nothing"Page_Load rebinds after, or the handler's work is overwritten
"The dropdown keeps resetting"The list is rebound on every postback, losing the selection
"The page is slow"Every control rebinds from the database on every click

Rule: anything that populates a control from a data source belongs inside If Not IsPostBack. Anything that must run on every request — security checks, reading a query string — goes outside it.

Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
' Every request, including postbacks
If Session("SchoolId") Is Nothing Then
Response.Redirect("~/Login.aspx")
Return
End If

' First request only
If Not IsPostBack Then
LoadClassDropDown()
LoadStudents()
End If
End Sub

Where control values are and are not available

Protected Sub Page_Init(sender As Object, e As EventArgs) Handles Me.Init
' txtSearch.Text is EMPTY here — postback data has not been applied yet
End Sub

Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
' txtSearch.Text HAS the user's value on a postback
End Sub

Reading a control's value in Init gives you the wrong answer, silently. When a value is unexpectedly empty, check which event you are in before checking anything else.

The reverse trap: changing a control in PreRender overrides anything a click handler did, because PreRender runs later. Code that "works but the label always shows the old value" often has a stale assignment in PreRender.

Dynamic controls

Controls created in code do not survive a postback unless they are recreated at the right moment.

' Wrong — the button exists on the first request, but its click never fires
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
Dim button As New Button()
button.ID = "btnDynamic"
button.Text = "Approve"
AddHandler button.Click, AddressOf Dynamic_Click
pnlActions.Controls.Add(button)
End Sub

The control must exist before ViewState is restored, which means Init or PreInit:

Protected Sub Page_Init(sender As Object, e As EventArgs) Handles Me.Init
Dim button As New Button()
button.ID = "btnDynamic" ' a stable ID is required
button.Text = "Approve"
AddHandler button.Click, AddressOf Dynamic_Click
pnlActions.Controls.Add(button)
End Sub

Two rules for dynamic controls:

  • Create them in Init, on every request, including postbacks. Recreating them only when Not IsPostBack guarantees the event never fires.
  • Give every one a stable, deterministic ID. ASP.NET matches posted data to controls by id. An id derived from a loop counter that changes between requests loses the match.

A dynamic button whose click handler never runs is nearly always one of these two.

Tracing the lifecycle

Turn on page tracing to see the actual order and timings:

<%@ Page Trace="true" TraceMode="SortByTime" ... %>

Or application-wide in Web.config:

<system.web>
<trace enabled="true" pageOutput="true" requestLimit="40" localOnly="true" />
</system.web>

The trace output appends to the page and shows every lifecycle event with elapsed time, the full control tree with each control's rendered size and ViewState size, and every form value posted. It is the fastest way to answer "is this control even being created?" and "why is this page 2 MB?".

localOnly="true" matters — trace output can expose session values and form data. Never enable it for remote users.

For a quick check without tracing, log the sequence:

Protected Sub Page_Init(sender As Object, e As EventArgs) Handles Me.Init
Debug.WriteLine("Init · IsPostBack=" & IsPostBack.ToString())
End Sub

Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
Debug.WriteLine("Load · IsPostBack=" & IsPostBack.ToString() &
" · txtSearch=" & txtSearch.Text)
End Sub

Protected Sub btnSearch_Click(sender As Object, e As EventArgs) Handles btnSearch.Click
Debug.WriteLine("Click · txtSearch=" & txtSearch.Text)
End Sub

Run it, click the button, and read the output window. Seeing Load print before Click once, on every postback, fixes the mental model permanently.

Errors you will hit

What you seeCauseFix
A dropdown resets its selection on every clickRebound in Page_Load without an IsPostBack checkWrap the binding in If Not IsPostBack
A click handler never runsThe control is created after the event stage, or was rebuiltCreate dynamic controls in Page_Init
Values are the old ones inside the handlerRead before ViewState and postback data were appliedRead in the handler, not in Page_Load
NullReferenceException in Page_Load on a dynamic controlControl not created yet at that stageMove creation to Page_Init
Changes to a control in Page_PreRender do not persistToo late for ViewState to save themSet them earlier

The IsPostBack check is the single most important line in Web Forms. Without it, Page_Load rebinds the control on every postback and destroys whatever the user just chose — before the click handler even runs.

Common mistakes

  • Binding data in Page_Load without an IsPostBack check
  • Assuming the click handler runs before Page_Load
  • Reading a control's value in Init and getting an empty string
  • Creating dynamic controls in Load instead of Init
  • Creating dynamic controls only when Not IsPostBack
  • Giving dynamic controls ids that change between requests
  • Setting a control's value in PreRender, overriding the click handler
  • Expecting a code-behind field to survive to the next request
  • Leaving Trace enabled where users can see it

Practice

Take a Web Forms page with a search filter and a dropdown. Add Debug.WriteLine to Page_Init, Page_Load, and the button's click handler, printing IsPostBack and the textbox value. Load the page, then click the button, and write down the exact output order.

Then run the debugging drill from the course — find why an event does not fire. Deliberately break it three ways and confirm the symptom each time: remove runat="server" from the button; create a dynamic button in Load instead of Init; and rebind the dropdown in Page_Load with no IsPostBack guard so the selection resets. Each produces a different failure, and recognising them on sight is the skill this article is for.

Finally, enable Trace="true" on one page and record its total ViewState size and its three slowest lifecycle stages.

You can now

  • State the lifecycle order from memory
  • Explain why Page_Load runs before every click handler
  • Use IsPostBack correctly, and say what breaks without it
  • Create dynamic controls at the right stage
  • Diagnose an event that does not fire

Review questions

  1. Why does data binding without an IsPostBack check break a search filter?
  2. Why is txtSearch.Text empty in Page_Init but populated in Page_Load?
  3. Why must dynamic controls be created in Init and on every request?
  4. Why does setting a label's text in PreRender override the click handler?

Next: Server controls and events