掌握 **伺服器控制項生命週期** 是開發高品質自訂控制項、解決 ViewState 問題、效能優化的基石。ASP.NET 頁面與控制項共享同一生命週期管線,控制項在各階段的行為與責任截然不同。以下系統解析 11 大階段,附關鍵方法、最佳實務與常見錯誤。
| 階段 | 事件/方法 | 核心任務 | 關鍵提醒 |
|---|---|---|---|
| 1. 初始化 | Init / OnInit() | 建立子控制項、設定基礎屬性、掛載事件、TrackViewState() 開始追蹤 | 勿存取 ViewState/ControlState/其他控制項 (尚未建立) |
| 2. 載入 ViewState | LoadViewState() | 從 ViewState 還原屬性值 (僅 Postback) | 複雜型別/集合需覆寫;預設已處理基礎型別 |
| 3. 處理回發資料 | LoadPostData() (IPostBackDataHandler) | 讀取表單欄位值更新控制項狀態 (如 TextBox、DropDownList) | 回傳 true 表示值有變更,觸發 RaisePostDataChangedEvent |
| 4. 載入 | Load / OnLoad() | **主要業務邏輯執行點**:資料繫結、查詢、驗證、狀態計算 | 所有控制項已建立、ViewState 已還原、表單值已同步 |
| 5. 回發變更通知 | RaisePostDataChangedEvent() (IPostBackDataHandler) | 通知控制項值已變更 (觸發 TextChanged、SelectedIndexChanged 等) | 僅 LoadPostData 回傳 true 的控制項會收到 |
| 6. 處理回發事件 | RaisePostBackEvent() (IPostBackEventHandler) | 處理按鈕點擊、命令事件 (Click、Command、RowCommand) | 此階段才執行開發者寫的 btn_Click 事件處理器 |
| 7. 預呈現 | PreRender / OnPreRender() | **最後修改控制項狀態的機會**:資料最終整理、動態產生子控制項、註冊腳本 | 此階段修改 ViewState 會被保存;Render 階段修改會遺失 |
| 8. 儲存狀態 | SaveViewState() | 將需持久化的屬性序列化進 ViewState | 複雜型別需覆寫;預設處理基礎型別、集合、可序列化物件 |
| 9. 呈現 | Render / RenderControl() | 輸出 HTML 至 HtmlTextWriter | **勿修改狀態**;使用 Writer 方法而非字串拼接 |
| 10. 釋放 | Dispose / Dispose() | 釋放非受控資源 (資料庫連線、檔案控制代碼、GDI+ 物件) | 實作 IDisposable 模式;不要在這裡存取控制項樹 |
| 11. 卸載 | Unload / OnUnload() | 最終清理 (極少使用) | 慣用 Dispose;Unload 時控制項樹可能已部分銷毀 |
protected override void OnInit(EventArgs e)
{
base.OnInit(e); // 必須呼叫基底
// 1. 建立子控制項 (確保 ID 固定,利於 ViewState 對應)
_txtName = new TextBox { ID = "txtName" };
_btnSubmit = new Button { ID = "btnSubmit", Text = "送出" };
_btnSubmit.Click += BtnSubmit_Click;
// 2. 加入控制項集合
Controls.Add(_txtName);
Controls.Add(_btnSubmit);
// 3. 開始追蹤 ViewState (基底已呼叫 TrackViewState)
// 覆寫 TrackViewState 僅在複雜屬性 (如自訂集合) 時需要
}
[Serializable]
private class CustomState
{
public Dictionary DynamicProperties { get; set; }
public List SelectedIds { get; set; }
}
private CustomState _state = new CustomState();
protected override object SaveViewState()
{
// 回傳物件將自動序列化進 ViewState
return new Pair(base.SaveViewState(), _state);
}
protected override void LoadViewState(object savedState)
{
if (savedState == null) return;
var pair = (Pair)savedState;
base.LoadViewState(pair.First);
_state = (CustomState)pair.Second;
}
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
// 1. 確保子控制項已建立 (CreateChildControls)
EnsureChildControls();
// 2. 註冊客戶端腳本 (位置正確、不重複)
if (!Page.ClientScript.IsClientScriptBlockRegistered(GetType(), "MyControlScript"))
{
Page.ClientScript.RegisterClientScriptBlock(GetType(), "MyControlScript",
"function myFunc() { ... }", true);
}
// 3. 最後調整屬性、資料繫結結果
if (DataSource != null && !_dataBound)
{
DataBind();
_dataBound = true;
}
}
protected override void Render(HtmlTextWriter writer)
{
// 1. 使用 Writer 方法,支援縮排、編碼、屬性編碼
writer.AddAttribute(HtmlTextWriterAttribute.Id, ClientID);
writer.AddAttribute(HtmlTextWriterAttribute.Class, "my-control");
writer.RenderBeginTag(HtmlTextWriterTag.Div);
// 2. 讓子控制項自我渲染
_txtName.RenderControl(writer);
_btnSubmit.RenderControl(writer);
writer.RenderEndTag(); // Page.Init Control.Init (樹狀遞迴) Page.LoadViewState Control.LoadViewState Page.LoadPostData Control.LoadPostData (IPostBackDataHandler) Page.Load Control.Load Page.LoadComplete Page.PreRender Control.PreRender Page.SaveViewState Control.SaveViewState Page.Render Control.Render Page.Unload Control.Unload
| 錯誤 | 後果 | 正確做法 |
|---|---|---|
| 在 Init 存取 ViewState | Null/例外,ViewState 尚未載入 | 在 Load 或 PreRender 存取 |
| 在 Render 修改屬性 | 修改不會持久化,下次 Postback 遺失 | 在 PreRender 前修改 |
| 未呼叫 base.OnXxx() | 基底邏輯中斷 (ViewState 追蹤、事件掛載失效) | 永遠先呼叫 base.OnXxx(e) |
| 動態建立控制項未固定 ID | ViewState 無法對應,Postback 狀態錯亂 | Init 階段建立並指定固定 ID |
| 大量資料存入 ViewState | 頁面膨脹、傳輸慢、序列化慢 | 改用 Session/Cache/資料庫,ViewState 只存識別碼 |
| 不實作 IDisposable 釋放非受控資源 | 記憶體洩漏、連線池耗盡 | 實作 Dispose 模式,釋放 DbConnection、GDI+、檔案控制代碼 |
控制項生命週期是 **嚴格有序的管線**。理解各階段的**職責邊界**、**可用資源**、**狀態持久化時機**,能寫出健壯、高效、可維護的自訂控制項。建議將上表列印貼在工位,開發/除錯時隨時對照。