2009/9/22

[入門] 給 ASP.NET 新手的建議 - 關於網頁生命週期

我當過電腦講師、寫過不少文章、也在國內外各個 ASP.NET 討論區轉戰了好幾年, 接觸過無以數計想學網站設計的新手或學生。在這麼多人裡面, 我大致上可以歸納出一種或少數幾種性質蠻接近的族群, 這些人有共同的特色、遇到相同的問題、也有相同的盲點。我現在要在這裡給這些人提供幾點小小的建議。

這些人有個共同的特色, 那就是不完全清楚網站運作的基本邏輯。我時常喜歡打個比方: 以電視 (尤其是陰極射線管的電視) 為例, 我們在看電視時, 總以為電視上的圖像總是永遠是連續的, 但其實電視的畫面是從上到下、從左到右一點一點畫出來的, 每秒畫三十幀 (略估), 然後靠畫面上螢光物質的暫時殘留現象, 讓人眼 (其實人眼也有視覺殘留效應) 看不出來它其實是每秒三十次一直重畫的。

網頁也有類似的機制。因為網頁對於伺服器而言是幾近於完全無保留狀態 (Stateless) 的, 所以網頁的每次重新載入 (Postback) 都和第一次載入時一樣; 你反正每次都要從伺服器端把整個網頁都重新傳過來一次。當然, 這裡講的是在 AJAX 技術被發明以前; 為求單純, 涉及 AJAX 的部份先暫且跳過。

因此, 在 PostBack 之後, 你以為你還在看同一個網頁, 但是整個網頁在背景中已經來回重新傳過了一遍。歸功於網頁機制的某些特殊作用, 使得你完全看不出來這個網頁其實已經在背景中重新傳過一次了。所以當 Postback 程序完成, 使用者根本感覺不到網頁有重送這回事, 因為他輸入的文字還在、下拉式選單也停留在他上次選擇的項目, 甚至畫面也停留在他原來捲動的地方。

有很多 ASP.NET 初學者時常想不通, 為什麼他的網頁在按下某個按鈕之後, 原來打進去的文字不見了、選單跑掉了, 甚至連已經加進去的控制項也消失了... 如果你確實認識了網頁的 Postback 機制, 你就應該立即可以判斷問題出在什麼地方。直接了當的講, 就是網頁重畫的部份沒有寫好。

相對的, 視窗應用程式 (Windows Form) 是沒有所謂重畫這一回事的。你的任何控制項加入視窗之後就在那裡了, 既不會被擦掉, 當然也就無需重畫。但是, 如果你從未寫過網站應用程式, 那麼我保證你會遇到的最大問題就在所謂的網頁生命週期 (Life Cycle) 這件事上面 (不管有沒有 Postback)。

如果你想快速了解一下 ASP.NET 的 Life Cycle, 你可以直接 Google 一下, 看看裡面的一些圖片, 或者直接到 MSDN 網站上看看正式的文件。

不過, 你也可以在 .aspx 檔案開頭的 <@Page 鈙述中加入 Trace="true" 指令, 那麼你的網頁的下方就會出現網頁生命週期中間各個網頁事件發生的順序。

如果你只是加上了 Trace="true" 指令, 那麼上圖和你在自己電腦上看到的畫面會略有不同, 因為我在程式中另外有加上 Trace.Warn() 指令, 它會額外輸出一些資訊供作參考。這些自訂的除錯資訊只會出現在加上了 Trace="true" 的網頁裡, 普通時候是看不見的。

為求各位的方便, 我寫了一個簡單的網頁, 讓它把所有可以輸出除錯訊息的網頁事件通通編號後列出來:

.aspx

<%@ Page AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" Language="C#"
    Trace="true" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        Text:
        <asp:TextBox ID="TextBox1" runat="server" ondatabinding="TextBox1_DataBinding"
            ondisposed="TextBox1_Disposed" oninit="TextBox1_Init" onload="TextBox1_Load"
            onprerender="TextBox1_PreRender" ontextchanged="TextBox1_TextChanged"
            onunload="TextBox1_Unload" AutoPostBack="True"></asp:TextBox>
        <br />
        Label:
        <asp:Label ID="Label1" runat="server" ondatabinding="Label1_DataBinding"
            ondisposed="Label1_Disposed" oninit="Label1_Init" onload="Label1_Load"
            onprerender="Label1_PreRender" onunload="Label1_Unload" Text="Label"></asp:Label>
    DropDownList:
        <asp:DropDownList ID="DropDownList1" runat="server"
            ondatabinding="DropDownList1_DataBinding" ondatabound="DropDownList1_DataBound"
            ondisposed="DropDownList1_Disposed" oninit="DropDownList1_Init"
            onload="DropDownList1_Load" onprerender="DropDownList1_PreRender"
            onselectedindexchanged="DropDownList1_SelectedIndexChanged"
            ontextchanged="DropDownList1_TextChanged" onunload="DropDownList1_Unload"
            AutoPostBack="True">
            <asp:ListItem>Item 1</asp:ListItem>
            <asp:ListItem>Item 2</asp:ListItem>
            <asp:ListItem>Item 3</asp:ListItem>
        </asp:DropDownList>
        <br />
        <asp:Button ID="Button1" runat="server" Text="Button" />   
    </div>
    </form>
</body>
</html>

.aspx.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class _Default : System.Web.UI.Page
{
    int _inum = 0;
    string inum
    {  
        get { return (++_inum).ToString() + ". "; }
    }
    protected void Page_PreInit(object sender, EventArgs e)
    {
        TraceWarn("Page_PreInit");
    }
    protected void Page_Init(object sender, EventArgs e)
    {
        TraceWarn("Page_Init");
    }
    protected void Page_InitComplete(object sender, EventArgs e)
    {
        TraceWarn("Page_InitComplete");
    }
    protected void Page_PreLoad(object sender, EventArgs e)
    {
        TraceWarn("Page_PreLoad");
    }
    protected void Page_Load(object sender, EventArgs e)
    {
        TraceWarn("Page_Load");
    }
    protected void Page_LoadComplete(object sender, EventArgs e)   
    {
        TraceWarn("Page_LoadComplete");
    }
    protected void Page_PreRender(object sender, EventArgs e)
    {
        TraceWarn("Page_PreRender");
    }
    protected void Page_PreRenderComplete(object sender, EventArgs e)
    {
        TraceWarn("Page_PreRenderComplete");
    }
    protected void Page_SaveStateComplete(object sender, EventArgs e)
    {
        TraceWarn("Page_SaveStateComplete");
    }
    protected void Page_Unload(object sender, EventArgs e)
    {
        // No message can be shown at this stage
    }
    protected void TextBox1_TextChanged(object sender, EventArgs e)
    {
        TraceWrite("TextBox1_TextChanged");
    }
    protected void TextBox1_DataBinding(object sender, EventArgs e)
    {
        TraceWrite("TextBox1_DataBinding");
    }
    protected void TextBox1_Disposed(object sender, EventArgs e)
    {
        TraceWrite("TextBox1_Disposed");
    }
    protected void TextBox1_Init(object sender, EventArgs e)
    {
        TraceWrite("TextBox1_Init");
    }
    protected void TextBox1_Load(object sender, EventArgs e)
    {
        TraceWrite("TextBox1_Load");
    }
    protected void TextBox1_PreRender(object sender, EventArgs e)
    {
        TraceWrite("TextBox1_PreRender");
    }
    protected void TextBox1_Unload(object sender, EventArgs e)
    {
        TraceWrite("TextBox1_Unload");
    }
    protected void Label1_DataBinding(object sender, EventArgs e)
    {
        TraceWrite("Label1_DataBinding");
    }
    protected void Label1_Disposed(object sender, EventArgs e)
    {
        TraceWrite("Label1_Disposed");
    }
    protected void Label1_Init(object sender, EventArgs e)
    {
        TraceWrite("Label1_Init");
    }
    protected void Label1_Load(object sender, EventArgs e)
    {
        TraceWrite("Label1_Load");
    }
    protected void Label1_PreRender(object sender, EventArgs e)
    {
        TraceWrite("Label1_PreRender");
    }
    protected void Label1_Unload(object sender, EventArgs e)
    {
        TraceWrite("Label1_Unload");
    }
    protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
    {
        TraceWrite("DropDownList1_SelectedIndexChanged");
    }
    protected void DropDownList1_TextChanged(object sender, EventArgs e)
    {
        TraceWrite("DropDownList1_TextChanged");
    }
    protected void DropDownList1_DataBinding(object sender, EventArgs e)
    {
        TraceWrite("DropDownList1_DataBinding");
    }
    protected void DropDownList1_DataBound(object sender, EventArgs e)
    {
        TraceWrite("DropDownList1_DataBound");
    }
    protected void DropDownList1_Disposed(object sender, EventArgs e)
    {
        TraceWrite("DropDownList1_Disposed");
    }
    protected void DropDownList1_Init(object sender, EventArgs e)
    {
        TraceWrite("DropDownList1_Init");
    }
    protected void DropDownList1_Load(object sender, EventArgs e)
    {
        TraceWrite("DropDownList1_Load");
    }
    protected void DropDownList1_PreRender(object sender, EventArgs e)
    {
        TraceWrite("DropDownList1_PreRender");
    }
    protected void DropDownList1_Unload(object sender, EventArgs e)
    {
        TraceWrite("DropDownList1_Unload");
    }
    protected void TraceWrite(string message)
    {
        Trace.Write(inum  + message);
    }
    protected void TraceWarn(string message)
    {
        Trace.Warn(inum + message);
    }
}

你可以把這個網頁放到你的網站裡面, 下次如果你想列出網頁事件的執行順序時, 就可以把它拿來執行一下, 看看各事件之間的相對次序。

在這個網頁中, 除了基本的網頁事件 (在程式中我在這類事件中使用了 Trace.Warn 指令, 所以它們的輸出文字都是紅色) 之外, 我另外加上了一個 TextBox 控制項、一個 Label 控制項和一個 DropDownList 控制項。基本上, 整個網頁中各事件的執行順序是這樣的:

  1. Page_PreInit()

  2. 各控制項的 Init()

  3. Page_Init()

  4. Page_InitComplete()

  5. Page_PreLoad()

  6. Page_Load()

  7. 各控制項的 Load()

  8. Page_LoadComplete()

  9. Page_PreRender()

  10. 各控制項的 PreRender()

  11. Page_PreRenderComplete()

  12. Page_SaveStateComplete()

  13. Page_Unload()

各位可以看出, 其實 Page_Load() 事件的發生是很後面的, 所以很多人在 Page_Load() 程序中才在頁面上動態加入控制項, 卻在 Paeg.Load 事件之前就想存取這些控制項, 無怪乎出現 Null Reference 的糗事。

對我個人而言, 我通常會避免動態加入控制項。雖然說我很清楚如何做, 但是我剛剛重新檢查了一下我所有的程式碼, 我還真的沒這麼做過。這表示即使不使用動態加入控制項的方法, 一樣可以把網頁寫好。

此外, 有很多初學者的問題都出現在 GridView/FormView 等容器的編輯樣板中。由於這些容器有很多事件 (如 RowDataBound, RowCommand 等), 但在樣板中嵌入的控制項到底在什麼時候才能被使用 FindControl() 指令找到呢? 如果你剛好有這樣的疑問, 同樣的, 請把上面程式修改一下, 同樣進入 Trace 模式觀察一下, 我相信你很快就可以找到答案。

沒有留言:

張貼留言