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
IsPostBackand 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
| Term | Meaning |
|---|---|
| Postback | A POST request a page sends to itself, carrying its own state |
| Initial request | The first GET of a page — no ViewState, no posted values |
| Round trip | One full request/response cycle to the server |
| Lifecycle | The 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
| # | Event | What is available | What belongs here |
|---|---|---|---|
| 1 | PreInit | Nothing restored | Set master page, theme, create dynamic controls |
| 2 | Init | Controls exist, no ViewState | Wire dynamic handlers |
| 3 | InitComplete | — | — |
| 4 | ViewState loaded | Control properties restored | (framework) |
| 5 | Postback data applied | Posted values in controls | (framework) |
| 6 | PreLoad | Everything restored | Rare |
| 7 | Load (Page_Load) | Full state and posted values | Most of your code |
| 8 | Control events | — | btnSearch_Click, SelectedIndexChanged |
| 9 | LoadComplete | All events done | Work depending on every handler |
| 10 | PreRender | Final chance to change output | Late data binding |
| 11 | ViewState saved | — | (framework) |
| 12 | Render | — | (framework) |
| 13 | Unload | Page being discarded | Cleanup — cannot change output |
Two orderings decide almost everything:
- ViewState and posted values are restored before
Page_Load. ReadingtxtSearch.TextinPage_Loadon a postback gives you what the user typed. Page_Loadruns 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 as | Actual 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 whenNot IsPostBackguarantees 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 see | Cause | Fix |
|---|---|---|
| A dropdown resets its selection on every click | Rebound in Page_Load without an IsPostBack check | Wrap the binding in If Not IsPostBack |
| A click handler never runs | The control is created after the event stage, or was rebuilt | Create dynamic controls in Page_Init |
| Values are the old ones inside the handler | Read before ViewState and postback data were applied | Read in the handler, not in Page_Load |
NullReferenceException in Page_Load on a dynamic control | Control not created yet at that stage | Move creation to Page_Init |
Changes to a control in Page_PreRender do not persist | Too late for ViewState to save them | Set 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_Loadwithout anIsPostBackcheck - Assuming the click handler runs before
Page_Load - Reading a control's value in
Initand getting an empty string - Creating dynamic controls in
Loadinstead ofInit - 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
Traceenabled 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_Loadruns before every click handler - Use
IsPostBackcorrectly, and say what breaks without it - Create dynamic controls at the right stage
- Diagnose an event that does not fire
Review questions
- Why does data binding without an
IsPostBackcheck break a search filter? - Why is
txtSearch.Textempty inPage_Initbut populated inPage_Load? - Why must dynamic controls be created in
Initand on every request? - Why does setting a label's text in
PreRenderoverride the click handler?