開發 ASP.NET Web Form 的工程師對於網頁生命週期(Page Life Cycle)一定要熟記在心中,也要知道每個循環中的會發生的個事件的主要目的,才不會在需求要修改或是出現Bug時不知道該往哪裡找問題。不過,講是這樣講其實 ASP.NET 的網頁生命週期需要注意的細節是滿多的,而且一個週期內會發生的事件可能比你想像中的還要多。

一般來說網頁生命週期會經過

網頁要求 => 開始 => 初始化 => 載入 => 回傳事件處理 => 呈現 => 卸載

資料來源 表一:MSDN 一般網頁生命週期階段 Page Life Cycle

階段說明
網頁要求在網頁生命週期開始前會發生網頁要求。當使用者要求網頁時,ASP.NET 會判斷是否需要剖析和編譯網頁 (因此開始網頁的生命週期),或是可以在不執行網頁的情況下,傳送網頁的快取版本做為回應。
開始在開始階段,會先設定網頁屬性,如 Request 與 Response。在這個階段中,網頁也會判斷要求是否為回傳或是新的要求,然後設定 IsPostBack 屬性。網頁同時也會設定 UICulture 屬性。
初始化在網頁初始化期間,可以使用網頁上的控制項,並且設定每個控制項的 UniqueID 屬性。如果適用,也會將主版頁面與佈景主題套用到網頁。如果目前的要求是回傳,則尚未載入回傳資料,並且控制項屬性值並未還原至檢視狀態提供的值。
載入在載入期間,如果目前的要求是回傳,就會使用從檢視狀態和控制項狀態復原的資訊載入控制項屬性。
回傳事件處理如果是回傳要求,會先呼叫控制項事件處理常式,然後才會呼叫所有驗證程式控制項的 Validate 方法,以設定個別驗證程式控制項與網頁的 IsValid 屬性。
呈現在呈現前,會儲存網頁和所有控制項的檢視狀態。在呈現階段,網頁會呼叫每個控制項的 Render 方法,藉此提供文字寫入器將其輸出寫入網頁之 Response 屬性的 OutputStream 物件。
卸載完整呈現網頁之後,會引發 Unload 事件,然後傳送至用戶端予以捨棄。此時將會執行網頁屬性 (如 Response 與 Request) 的卸載及清除工作。

而在這樣的生命週期階段中會有數十個事件依序發生,而不論是作為網頁容器的 Page 類別或是使用者控制項(User Control)或是伺服器控制項(Server Constrol)幾乎都有相對應的事件。 Page 頁面是每個 Web Form 都會繼承的類別,依據需求上面也可以放置自行撰寫的使用者控制項(User Control),或是裡面也可以直接擺上 Server Control,因此我們可以把這個類別是為一個容器,裡面可以擺放很多顯示用的控制項,而這個控制項裡會有很多事件會在一個 request 進來後依序引發,以下就是網頁生命週期的「生命週期事件」:

  • OnPreInit

這個事件會在初始化之前就引發,通常會拿來檢查 PostBack 屬性、建立或是重建動態控制項、動態設定主版頁面、動態設定 Theme 屬性、讀取貨設定設定黨屬性值。

  • OnInit

初始化所有控制項並套用任何面板設定後引發。個別控制項的 OnInit 事件會在容器前執行。可以在這個時候讀取或是初始化控制項的屬性。

  • OnInitComplete

發生在網頁初始化結束時。

  • OnPreLoad

這邊會於載入本身以及所包含的控制項的 ViewState,以及處理 request 執行個體中所附的回傳資料後引發。 若有自定驗證的話通常我會在這邊來進行,就不用載入太多東西。

  • OnLoad

這邊容器會呼叫本身的 OnLoad 方法,然後以遞迴的方式對每一個控制項執行相同的動作。(因此控制項的 Load 會比較晚執行)通常我會於此設定控制項中的屬性,以及重建資料庫連線。

  • OnLoadComplete

於 OnLoad 事件處理完畢後引發。

  • OnPreRender

容器會呼叫自己的 PreRender 事件,然後也是以遞迴的方式對每一個控制項執行相同的動作。這個事件可以在呈現(Render)階段前對控制項的屬性做最後的變更。 通常我會在這邊把需要的 javascript 或 CSS 給引入或是對顯示邏輯進行修改。

  • OnSaveStateComplete

這個事件是在儲存網頁所有控制項的 ViewState 後引發,因為是在呼叫Render方法之前,因此所做的任何變更依舊會影響控制項的呈現,但是因為 ViewState 已經儲存了,所以下一次的 PostBack 並不會擷取這些變更! (因此PreRender才會被視為呈現前最後一個可以變更控制項的事件,因為此時變更後會被儲存下來!)

  • Render

這不是個事件…在呈現這個階段每個控制項都會呼叫自己的Render方法來產生各自的HTML Tag。

  • OnUnload

會先由控制項引發,控制項都引發完畢後才是容器引發。通常在這邊會對頁面使用到的特定資源進行釋放,像是與資料庫的連線或是關閉開啟的檔案。

以上資料來源 MSDN 生命週期事件 + 我的理解

當然上面這些說明的文件看完之後還是需要手動來驗證一下才比較有感覺! 就讓我們來驗證一下吧!

驗證

實驗一

  • 首先我們先新增一個空白 .NET Web Form 專案,並且新增項目選擇 Web Form,命名為 Index.aspx。

  • 打開 Package Manager Console 安裝 NLog Install-Package NLog Install-Package NLog.Config

    這邊安裝 NLog 是為了用來記錄每個事件的觸發順序使用。

  • 修改 NLog.config

1
2
3
4
5
6
7
8
<targets>
    <target xsi:type="File" name="test" fileName="${basedir}/logs/${shortdate}.log"
            layout="${longdate} ${message}"/>
  </targets>

  <rules>
    <logger name="test" minlevel="Trace" writeTo="test" />
</rules>
  • 在 Index.aspx.cs 中寫下日誌
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
using System;
using NLog;
public partial class Index : System.Web.UI.Page
{
    private Logger _logger = LogManager.GetLogger("test");

    protected override void OnPreInit(EventArgs e)
    {
        _logger.Trace("OnPreInit event execute!");
        base.OnPreInit(e);
    }

    protected override void OnInit(EventArgs e)
    {
        _logger.Trace("OnInit event execute!");
        base.OnInit(e);
    }

    protected override void OnInitComplete(EventArgs e)
    {
        _logger.Trace("OnInitComplete event execute!");
        base.OnInitComplete(e);
    }

    protected override void OnPreLoad(EventArgs e)
    {
        _logger.Trace("OnPreLoad event execute!");
        base.OnPreLoad(e);
    }

    protected override void OnLoad(EventArgs e)
    {
        _logger.Trace("OnLoad event execute!");
        base.OnLoad(e);
    }

    protected override void OnLoadComplete(EventArgs e)
    {
        _logger.Trace("OnLoadComplete event execute!");
        base.OnLoadComplete(e);
    }

    protected override void OnPreRender(EventArgs e)
    {
        _logger.Trace("OnPreRender event execute!");
        base.OnPreRender(e);
    }

    protected override void OnPreRenderComplete(EventArgs e)
    {
        _logger.Trace("OnPreRenderComplete event execute!");
        base.OnPreRenderComplete(e);
    }

    protected override void OnSaveStateComplete(EventArgs e)
    {
        _logger.Trace("OnSaveStateComplete event execute!");
        base.OnSaveStateComplete(e);
    }

    protected override void OnUnload(EventArgs e)
    {
        _logger.Trace("OnUnload event execute!");
        base.OnUnload(e);
        _logger.Trace("-------------------");
    }
}
  • 按下F5成功執行後直接關起來

  • 觀察寫下來的Log檔

Page_log

這邊我們可以發現頁面的事件順序大致上就是先初始化、載入、呈現最後就是卸載。 但若我們在頁面上面多加一個使用者控制項(User Constrol)呢?

實驗二

  • 在原本的專案中新增一個 ascx 檔,並且附上一個 Server Control。

  • 新增如下內容

1
2
3
4
5
<asp:TextBox ID="ucTextBox1" runat="server" 
             OnInit="ucTextBox1_Init"
             OnLoad="ucTextBox1_Load"
             OnPreRender="ucTextBox1_PreRender"
             OnUnload="ucTextBox1_Unload"></asp:TextBox>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public partial class UserControl1 : System.Web.UI.UserControl
{
    private Logger _logger = LogManager.GetLogger("test");

    protected override void OnInit(EventArgs e)
    {
        _logger.Trace("User Control OnInit event execute!");
        base.OnInit(e);
    }

    protected override void OnLoad(EventArgs e)
    {
        _logger.Trace("User Control OnLoad event execute!");
        base.OnLoad(e);
    }

    protected override void OnPreRender(EventArgs e)
    {
        _logger.Trace("User Constrol OnPreRender event execute!");
        base.OnPreRender(e);
    }

    protected override void OnUnload(EventArgs e)
    {
        _logger.Trace("User Constrol OnUnload event execute!");
        base.OnUnload(e);
    }

    protected override void OnDataBinding(EventArgs e)
    {
        _logger.Trace("User Control OnDataBinding event execute!");
        base.OnDataBinding(e);
    }

    #region 伺服器控制項的事件
    protected void ucTextBox1_Unload(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 Unload event execute!");
    }

    protected void ucTextBox1_PreRender(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 PreRender event execute!");
    }

    protected void ucTextBox1_Load(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 Load event execute!");
    }

    protected void ucTextBox1_Init(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 Init event execute!");
    } 
    #endregion
}
  • 按下F5成功執行後直接關起來

  • 觀察日誌

Pagelogwithusercontrol

這邊我們可以觀察到初始化時會是控制項的初始化先進行,然後才是 User Control 的初始化,最後是 Page 的初始化(容器的初始化會比較晚)。 而在載入的時候會先由 Page 先行載入,然後是 User Control 的載入,最後才是伺服器控制項的載入(容器會先行載入)。 而在卸載的時候則是控制項先卸載,然後是 User Constrol,最後才是 Page(容器的卸載的比較晚)。

實驗三

現在我們在 Page 頁面也新增兩個 button 來觀察看看各控制項的事件引發順序。

  • 我們分別在使用者控制項(User Control)的前後增加一個伺服器控制項(Server Control)。現在Index.aspx就會變成下面這樣
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<form id="form1" runat="server">
    <div>
        <asp:Button ID="button1" runat="server" Text="Firstbutton" 
                    OnInit="button1_Init"
                    OnLoad="button1_Load"
                    OnPreRender="button1_PreRender"
                    OnUnload="button1_Unload" />
        <uc:UserControl1 ID="uc1" runat="server" Visible="true" />
        <asp:Button ID="button2" runat="server" Text="Secondbutton" 
                    OnInit="button2_Init"
                    OnLoad="button2_Load"
                    OnPreRender="button2_PreRender"
                    OnUnload="button2_Unload" />
    </div>
</form>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#region 伺服器控制項的事件
protected void button1_Init(object sender, EventArgs e)
{
    _logger.Trace("Fisrtbutton Init event execute!");
}

protected void button1_Load(object sender, EventArgs e)
{
    _logger.Trace("Fisrtbutton Load event execute!");
}

protected void button1_PreRender(object sender, EventArgs e)
{
    _logger.Trace("Fisrtbutton PreRender event execute!");
}

protected void button1_Unload(object sender, EventArgs e)
{
    _logger.Trace("Fisrtbutton Unload event execute!");
}

protected void button2_Init(object sender, EventArgs e)
{
    _logger.Trace("Secondbutton Init event execute!");
}

protected void button2_Load(object sender, EventArgs e)
{
    _logger.Trace("Secondbutton Load event execute!");
}

protected void button2_PreRender(object sender, EventArgs e)
{
    _logger.Trace("Secondbutton PreRender event execute!");
}

protected void button2_Unload(object sender, EventArgs e)
{
    _logger.Trace("Secondbutton Unload event execute!");
}
#endregion
  • 在兩個 button 的事件中寫下日誌,方法就跟上面的寫法一樣…

  • 按下F5之後直接關掉觀察日誌檔

Pagelogcomplex

  • 初始化

這邊我們可以觀察到初始化的時候會依照控制項出現的順序依序初始化,因此第一個 button 會先被初始化,然後是使用者控制項中的 textbox 初始化,接下來就是使用者控制項的初始化!接下來才是第二個 button 的初始化,最後才是 Page 頁面(基底容器) 的初始化。(個別控制項的初始化會在它的容器前執行)

  • 載入

載入時依據MSDN官方說法是容器會先控制項載入,我們來驗證吧~!這邊我們可以看到 Page 頁面的載入確實先觸發了!然後是第一個 button 的載入,接下來是使用者控制項的載入,接著使用者控制項中的 textbox 載入,最後是第二的 button 的載入。載入就一這樣的順序結束了!

  • 呈現(Render)

這邊很特別,我們可以看到這個 Render 的順序根本就是頁面從上到下的順序依序呼叫 PreRender,不過也難怪啦~畢竟這邊是要產生 HTML Tag 是最後要呈現在瀏覽器的樣子,會有這樣的行為其實不意外,要是順序不是這樣最後呈現可能也會怪怪的。

  • 卸載

卸載這邊則會讓控制項先行卸載,最後才是容器卸載。所以這邊第一個 button 就會先執行卸載,接下來是使用者控制項中的 textbox 卸載,接著是使用者控制項,然後是第二個 button,這些控制項都卸載後最後才是 Page 頁面的卸載。

結語

在撰寫 ASP.NET Web Form 時了解網頁生命週期是不可或缺的知識,他可以幫助你理解 Web Form 架構時的一些眉角,能夠在正確的時間點做正確的事情。雖然現在在強大的 javascript library 或 framework 的幫助下,已經讓網頁的使用者體驗愈趨近 desktop 導致 Web Form 的式微,但是偶而還是會有需要使用到的時候,到時候就會需要了解這方面的知識了。

參考資料