ASP.Net請求處理機制初步探索之旅(4):WebForm頁面生命週期

發表於2015-03-19

開篇:上一篇我們瞭解了所謂的請求處理管道,在眾多的事件中微軟開放了19個重要的事件給我們,我們可以注入一些自定義的業務邏輯實現應用的個性化設計。本篇,我們來看看WebForm模式下的頁面生命週期。

一、ASP.Net Page的兩個重要部分

在前面對於請求處理管道的介紹中,我們已經瞭解了一個ASP.NET WebForm頁面請求事件的整體流程。那麼,在其中一個最重要的部分就是ASP.NET Page頁面,但是我們並沒有對其進行詳細討論。因此,我們在此深入地瞭解一下ASP.NET頁面事件。

每一個ASP.NET Page頁都有2個部分:一個部分是在瀏覽器中進行顯示的部分,它包含了HTML標籤、viewstate形式的隱藏域 以及 在HTML input中的資料。當這個頁面被提交到伺服器時,這些HTML標籤會被建立到ASP.NET控制元件,並且viewstate還會和表單資料繫結在一起。另一個部分是在xxx.cs檔案中的進行業務邏輯操作的部分,一旦你在後置程式碼中得到所有的伺服器控制元件,你可以執行和寫入你自己的邏輯並呈現給客戶瀏覽器。

其中,後臺程式碼類是前臺頁面類的父類,前臺頁面類則是後臺程式碼類的子類。這一點,可以通過檢視每個aspx檔案中的頭部,我們都會看到以下的一句程式碼:

其中CodeBehind這個屬性定義了此aspx頁面的專屬後臺程式碼檔案的名稱,而Inherits這個屬性則定義了此aspx頁面所要繼承的父類的名稱(這也可以簡單地說明,aspx頁面會單獨生成一個類,與後臺程式碼類不重合在一起)。因此,aspx.cs就是aspx的後置處理程式碼,負責處理aspx中<%%>和runat=”server”的內容。

現在這些HTML控制元件會作為ASP.NET控制元件存活在伺服器上,ASP.NET會觸發一系列的事件,我們也可以在這些事件中注入自定義邏輯程式碼。根據你想要執行什麼樣的任務/邏輯,我們需要將邏輯合理地放入這些事件之中。

TIP:大部分的開發者直接使用Page_Load來幹所有的事情,但這並不是一個好的思路。因此,無論是填充控制元件、設定ViewState還是應用主題等所有發生在頁面載入中的所有事情。因此,如果我們能夠在合適的事件中放入邏輯,那麼毫無疑問我們程式碼將會乾淨很多。

二、ASP.Net Page的頁面事件流程

順序 事件名稱 控制元件初始化 ViewState可用 表單資料可用 什麼邏輯可以寫在這裡?
1 Init No No No 注意:你可以通過使用ASP.NET請求物件訪問表單資料等,但不是通過伺服器控制元件。
動態地建立控制元件,如果你一定要在執行時建立;任何初始化設定;母版頁及其設定。在這部分中我們沒有獲得viewstate、提交的資料值及已經初始化的控制元件。
2 Load View State Not guaranteed Yes Not guaranteed 你可以訪問View State及任何同步邏輯,你希望viewstate被推到後臺程式碼變數可以在這裡完成。
3 PostBackdata Not guaranteed Yes Yes 你可以訪問表單資料。任何邏輯,你希望表單資料被推到後臺程式碼變數可以在這裡完成。
4 Load Yes Yes Yes 在這裡你可以放入任何你想操作控制元件的邏輯,如從資料庫填充combox、對grid中的資料排序等。這個事件,我們可以訪問所有控制元件、viewstate、他們傳送過來的值。
5 Validate Yes Yes Yes 如果你的頁面有驗證器或者你想為你的頁面執行驗證,那就在這裡做吧。
6 Event Yes Yes Yes 如果這是通過點選按鈕或下拉選單的改變的一個回發,相關的事件將被觸發。與事件相關的任何邏輯都可以在這裡執行。

PS:這個事件想必很多使用WebForm的開發人員都很常用吧,是否記得那些Button1_Click(Object sender,EventArgs e)?

7 Pre-render Yes Yes Yes 如果你想對UI物件做最終的修改,如改變屬性結構或屬性值,在這些控制元件儲存到ViewState之前。
8 Save ViewState Yes Yes Yes 一旦對伺服器控制元件的所有修改完成,將會儲存控制元件資料到View State中。
9 Render Yes Yes Yes 如果你想新增一些自定義HTML到輸出,可以在這裡完成。
10 Unload Yes Yes Yes 任何你想做的清理工作都可以在這裡執行。

三、反編譯探祕ASP.Net Page頁面生命週期

  前面我們簡單地瞭解了一下ASP.NET Page的頁面事件,現在我們來通過Reflector反編譯一下一個demo程式集,來感受一下ASP.NET Page的頁面生命週期。

3.1 準備一個ASP.NET專案

(1)假如我們有以下的名為Index的一個aspx頁面:


(2)Index所對應的後臺程式碼如下:

這裡,我們來重點關注一下這個方法:我們可以通過寫入以下程式碼,然後在aspx中<% GetDllInfo(); %>呼叫,它顯示了我們這個ASP.NET專案所屬的程式集在哪個位置?

瀏覽頁面,會顯示以下結果:通過下圖可以看到,我們的Index這個頁面會生成一個ASP.index_aspx的類,其父類是Index。

3.2 反編譯生成的臨時程式集

  ①將DLL拖到Reflector中進行檢視原始碼

通過上面顯示的路徑找到dll,並拖到反編譯工具(ILSpy或者Reflector,前者開源免費,後者已經收費,但天朝,你懂的。)進行檢視。通過下圖可以看出,頁面類aspx是後臺程式碼類所繫結的子類,它的名稱是aspx檔名加上“_aspx”字尾。因此,這裡也就解釋了為什麼在aspx中要訪問的方法必須是public和protected的訪問修飾符才可以。

  ②一個大型Control:Page類

從上面可以看出,頁面類繼承自後置程式碼類,而後置程式碼類又繼承自Page類。我們從上一篇管道可以知道,在請求處理管道的第8個事件中建立了Page類物件,那麼我們去看看Page類。

Page類繼承自TemplateControl,顧名思義,Page類是否就是一個模板控制元件呢?再看看TemplateControl類:

果不其然,其父類是Control類,Page就是一個封裝過的大控制元件!那麼,我們在Page中拖的那些runat=”server”的伺服器控制元件,又是儲存在哪裡的呢?

原來,在Control父類中,有一個Controls的屬性,它是一個控制元件的集合:Page中的所有控制元件,都會存在於這個集合中。

  ③頁面生命週期的入口:Page類的ProcessRequest方法

從上一篇請求處理管道中,我們知道在第11和第12個事件之間會呼叫Page類物件的ProcessRequest方法進入頁面生命週期。那麼我們來看看這個ProcessRequest方法:

從圖中可以看出,這個方法中首先通過呼叫頁面類物件(我們請求的頁面都是繼承於Page類的)重寫的FrameworkInitialize方法開始我們經常聽到的構造控制元件樹的過程。下面我們轉到index_aspx這個頁面類重寫的FrameworkInitialize方法中取看看是否是進行了構造頁面控制元件樹的操作:

  ④BuildControlTree:構造頁面控制元件樹

看到這裡,我們不由地想問,什麼是頁面控制元件樹?在一個aspx頁面中,runat=”server”的控制元件集合構成了如下圖所示的一棵頁面控制元件樹,他們被一一例項化,並依據層級關係儲存到了controls集合中。

瞭解了什麼是頁面控制元件樹,現在我們看看是如何來構造這棵樹的,通過檢視BuildControlTree方法,發現它呼叫了多個名為BuildControlX的方法,依次例項化我們頁面中所需的控制元件,並新增到控制元件集合中(這裡其實是將這些伺服器控制元件作為子控制元件新增到頁面(頁面本身就是一個大的控制元件)中,在樹形結構中Page就是一個根節點,而那些Page中的控制元件則是Page的孩子節點)。

那麼,這些BuildControlX(X代表數字)方法又在做些什麼事呢?我們可以通過檢視一個BuildControl方法,看看如何打造HtmlForm的:

可以看出,在構造HtmlForm控制元件的過程中,不僅為其設定了ID(_ctrl.ID=”formIndex”),還為其指定了渲染方法(通過設定委託_ctrl.SetRenderMethodDelegate())。又因為我們拖了一個TextBox和Button在其中,於是在例項化HtmlForm這個控制元件的途中,又去例項化TextBox和Button物件,並將其作為HtmlForm的子節點,形成一個層級關係。

  ⑤確定IsPostBack:是否第一次請求該頁面

現在重新回到Page類的ProcessRequest方法中,在建立頁面控制元件樹完成之後,開始進入一個ProcessRequestMain方法,這個方法則真正地開啟了頁面生命週期之門。

我們經常在Page_Load方法中使用Page.IsPostBack屬性來判斷請求是否是回發,那麼它是在哪裡設定的呢?原來,在ProcessRequestMain方法中:

  ⑥初始化操作:PreInit–>Init–>InitComplete

接下來就是初始化操作了,初始化操作分為了三個階段:預初始化、初始化(使用遞迴方式)、初始化完成。

預初始化主要利用App_Themes目錄中的內容進行初始化主題,並應用模板頁。

這裡我們主要看看初始化操作,通過檢視原始碼,可以看出,該方法通過遞迴呼叫子控制元件的初始化方法,完成了控制元件集合中所有控制元件的初始化操作。

再看看初始化方法中都做了哪些初始化操作,細細一看,原來就是為其動態地生成一個ID(control.GenerateAutomaticID()),然後將該控制元件的page指標指向當前Page頁等。PreLoad 預載入在 Load 事件之前對頁或控制元件執行處理,

  ⑦載入操作:(LoadState–>ProcessPostData–>)PreLoad–>Load–>

(ProcessPostData–>RaiseChangedEvents–>RaisePostBackEvent–>)LoadComplete

  • 首先看看(LoadState–>ProcessPostData)

初始化完成之後,ASP.NET會通過IsPostBack判斷是否是第一次請求,如果不是,那麼首先會載入ViewState並對回發的資料進行處理。

至於ViewState是什麼?又不瞭解的朋友,可以瀏覽我的另一篇博文:ASP.NET WebForm溫故知新:ViewState,這裡就不再贅述。這裡LoadAllState方法主要是將隱藏域中的_VIEWSTATE通過解碼獲取控制元件的狀態與資料資訊,而ProcessPostData方法則是進行了兩個部分的操作:一是將剛剛獲取到的各個控制元件的狀態與資料資訊填充到頁面控制元件樹中所對應的各個控制元件中去,二是對比控制元件狀態是否發生了改變?比如被點選了?被觸發了某個事件(例如TextChanged、SelectedIndexChanged等)?如有觸發事件,則把需要觸發事件的控制元件放到一個集合當中去。

  • 再來看看PreLoad–>Load

處理完ViewState後,就開始進行正式地載入操作了,如下程式碼所示:

在正式載入過程中也分為了兩個部分,一個是PreLoad預載入,另外一個則是重頭戲Load載入(通過方法名可以推斷,該方法是通過遞迴方式呼叫載入的)。首先,呼叫了OnPreLoad方法進行預載入操作,如果我們需要在 Load 事件之前對頁或控制元件(這時頁面控制元件樹已經構造完成)執行處理,就可以使用該事件。通過檢視原始碼,在PreLoad方法中會遍歷一個PreLoad事件集合(我們可以自定義注入我們想要的事件),然後依次執行委託所持有的事件。

PreLoad之後就是重頭戲,也是我們最為熟悉的Load了,在呼叫LoadRecursive()方法進入Load事件。

從上面可以看出:ASP.NET頁面首先呼叫自身的OnLoad方法以引發自身的Load事件,接著遞迴呼叫 Contorls 集合中各個控制元件的OnLoad方法以引發它們的Load事件。那麼,我們在頁面後置程式碼類中經常使用的Page_Load事件方法是在哪裡呼叫的呢?相信我們都有了答案,就在頁面自身的OnLoad方法中。

  • 二次經歷(ProcessPostData)

  載入結束後,會經歷第二次的處理回發資料的事件。那麼,我們不禁會問,為何還要第二次進行ProcessPostData方法的呼叫,我們剛剛不是都已經對ViewState進行了解碼並對應到了對應控制元件樹中的控制元件了嘛?這裡,我們首先看看下面一段程式碼:

假如我們要在Page_Load事件中動態地為Form新增一個TextBox控制元件,那麼之前的頁面控制元件樹就發生了改變,所以,這裡需要進行第二次的ProcessPostData方法,現在豁然開朗了吧。

  • 事件觸發(RaiseChangedEvents–>RaisePostBackEvent)

在第二次處理回發資料之後,會呼叫RaiseChangedEvents方法觸發控制元件狀態改變事件響應方法,例如TextBox_TextChanged、DropDownList_SelectedIndexChanged事件(這些事件中不包括Button_Click這種回發事件)等。檢視原始碼,通過遍歷狀態改變了的控制元件的集合(在第一次進行ProcessPostData時會檢查控制元件的狀態是否發生了改變,如果改變了就新增到一個集合中)

在處理完狀態改變事件響應方法後,會呼叫RaisePostBackEvent方法觸發例如按鈕控制元件的回發事件,例如Button_Click回發事件。

通過檢視程式碼,發現通過回傳的表單資料中根據__EVENTTARGET與__EVENTARGUMENT進行事件的觸發。我們可以通過檢視ASP.NET生成的前端HTML程式碼看到這兩個引數:下圖是一個設定為AutoPostBack的DropDownList控制元件,可以發現回發事件都是通過呼叫_doPostBack這個js程式碼進行表單的submit,而表單中最重要的兩個引數就是eventTarget和eventArgument。

通過瀏覽器提供的開發人員工具檢視資料請求報文,可以看到除了提交form中的input外,還提交了ASP.Net WebForm預置的一些隱藏欄位,而這些隱藏欄位則是WebForm為我們提供便利的基礎。比如EventTarget則記錄剛剛提交給伺服器的是哪個伺服器控制元件。

事件觸發完成之後,載入操作就完成了,這時會呼叫OnLoadComplete方法進行相關的事件,這裡就不再贅述了。

  • 頁面渲染 PreRender–>PreRenderComplete–>SaveState–>SaveStateComplete–>Render

這一階段就進入了頁面生命週期的尾巴,開始最終頁面的渲染流程:

這裡我們主要看看PreRenderSaveStateRender三個事件。

既然已經進入了頁面渲染階段,為何還要有一個PreRender預呈現階段?通過查詢資料,我們發現微軟這麼設計是為了給開發者提供一個最後一次更改頁面控制元件狀態或資料的機會,也就說:你可以再在這裡注入一個邏輯,最後一次改變控制元件值,或者統一地改變控制元件狀態為某個指定狀態。

然後就是SaveState,這個很好理解,也就說:剛剛給了你最後一次更改的機會結束後,我就要儲存最終的ViewState了。這裡需要注意的是:伺服器在向瀏覽器返回html之前,對ViewState中的內容是進行了Base64編碼的;

最後就是Render,進行最終的頁面呈現了,換句話說:就是拼接形成HTML字串。在這個階段,Page 物件會遍歷頁面控制元件樹並在每個控制元件上遞迴地呼叫此方法。所有 ASP.NET Web 伺服器控制元件都有一個用於寫出傳送給瀏覽器的控制元件標記的 Render 方法。通過對原始碼進行追蹤,可以看到以下程式碼:

在Render過程中,會判斷當前控制元件是否含有子控制元件集合,如果有,那麼遍歷各個子控制元件的Render方法進行HTML的渲染。可以想象,從頁面控制元件樹的根節點呼叫Render方法,會依次遞迴呼叫其所有子節點的Render方法,從而得到一個完整的HTML程式碼。

那麼,Render方法結束後,生成的HTML程式碼儲存到了哪裡呢?原來,Render方法的輸出會寫入Page類物件的 Response 屬性的 OutputStream 中,這就是最終的輸出流作為響應報文通過HTTP協議返回給瀏覽器端了。

  • 頁面解除安裝 Unload

自此,狹義上的頁面生命週期就結束了,但廣義上的頁面宣告週期事件還未結束,還會經歷一個UnLoad事件,該事件首先針對每個控制元件發生,繼而針對該頁發生。在控制元件中,使用該事件對特定控制元件執行最後清理,如關閉控制元件特定資料庫連線。對於頁自身,使用該事件來執行最後清理工作,如:關閉開啟的檔案和資料庫連線,或完成日誌記錄或其他請求特定任務。總而言之,Unload就是進行最後的清理工作,釋放資源。

總體概覽

一篇文章下來,已耗費了好多時間,如果你覺得對你有用,那就麻煩點個推薦吧。如果你覺得本文很爛,那點個反對也是可以的。後面Part 5會探祕ASP.NET MVC的頁面生命流程,今天就此停筆,謝謝!

參考資料

(1)農村出來的大學生,《ASP.NET網頁請求處理全過程(反編譯)》:http://www.cnblogs.com/poorpan/archive/2011/09/25/2190308.html

(2)我自己,《【翻譯】ASP.NET應用程式和頁面宣告週期》:http://www.cnblogs.com/edisonchou/p/3958305.html

(3)Shivprasad koirala,《ASP.NET Application and Page Life Cycle》:http://www.codeproject.com/Articles/73728/ASP-NET-Application-and-Page-Life-Cycle

(4)碧血軒,《ASP.NET頁面生命週期》:http://www.cnblogs.com/xhwy/archive/2012/05/20/2510178.html

(5)木宛城主,《ASP.NET那點不為人知的事兒》:http://www.cnblogs.com/OceanEyes/archive/2012/08/13/aspnetEssential-1.html

(6)千年老妖,《ASP.NET頁面生命週期》:http://www.cnblogs.com/hanwenhuazuibang/archive/2013/04/07/3003289.html

(7)MSDN,《Page事件》:http://msdn.microsoft.com/zh-cn/library/system.web.ui.page_events(v=vs.80).aspx

相關文章