開篇:上一篇咱們瞭解了所謂的請求處理管道,在衆多的事件中微軟開放了19個重要的事件給咱們,咱們能夠注入一些自定義的業務邏輯實現應用的個性化設計。本篇,咱們來看看WebForm模式下的頁面生命週期。html
(1)Part 1:前奏前端
(2)Part 2:核心web
(3)Part 3:管道數據庫
(4)Part 4:WebForm頁面生命週期瀏覽器
(5)Part 5:MVC頁面聲命週期服務器
在前面對於請求處理管道的介紹中,咱們已經瞭解了一個ASP.NET WebForm頁面請求事件的總體流程。那麼,在其中一個最重要的部分就是ASP.NET Page頁面,可是咱們並無對其進行詳細討論。所以,咱們在此深刻地瞭解一下ASP.NET頁面事件。app
每個ASP.NET Page頁都有2個部分:一個部分是在瀏覽器中進行顯示的部分,它包含了HTML標籤、viewstate形式的隱藏域 以及 在HTML input中的數據。當這個頁面被提交到服務器時,這些HTML標籤會被建立到ASP.NET控件,而且viewstate還會和表單數據綁定在一塊兒。另外一個部分是在xxx.cs文件中的進行業務邏輯操做的部分,一旦你在後置代碼中獲得全部的服務器控件,你能夠執行和寫入你本身的邏輯並呈現給客戶瀏覽器。ide
其中,後臺代碼類是前臺頁面類的父類,前臺頁面類則是後臺代碼類的子類。這一點,能夠經過查看每一個aspx文件中的頭部,咱們都會看到如下的一句代碼:工具
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="FirstPage.aspx.cs" Inherits="WebFormDemo.FirstPage" %>
其中CodeBehind這個屬性定義了此aspx頁面的專屬後臺代碼文件的名稱,而Inherits這個屬性則定義了此aspx頁面所要繼承的父類的名稱(這也能夠簡單地說明,aspx頁面會單獨生成一個類,與後臺代碼類不重合在一塊兒)。所以,aspx.cs就是aspx的後置處理代碼,負責處理aspx中<%%>和runat="server"的內容。post
如今這些HTML控件會做爲ASP.NET控件存活在服務器上,ASP.NET會觸發一系列的事件,咱們也能夠在這些事件中注入自定義邏輯代碼。根據你想要執行什麼樣的任務/邏輯,咱們須要將邏輯合理地放入這些事件之中。
TIP:大部分的開發者直接使用Page_Load來幹全部的事情,但這並非一個好的思路。所以,不管是填充控件、設置ViewState仍是應用主題等全部發生在頁面加載中的全部事情。所以,若是咱們可以在合適的事件中放入邏輯,那麼毫無疑問咱們代碼將會乾淨不少。
順序 | 事件名稱 | 控件初始化 | 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的頁面事件,如今咱們來經過Reflector反編譯一下一個demo程序集,來感覺一下ASP.NET Page的頁面生命週期。
(1)假如咱們有如下的名爲Index的一個aspx頁面:
<html xmlns="http://www.w3.org/1999/xhtml"> <head id="headIndex" runat="server"> <title>Index頁</title> </head> <body> <form id="formIndex" runat="server"> <div> 哈哈,我是ASP.Net WebForm,下面看個人表演。 <br /> <% for (int i = 0; i < 5; i++) { Response.Write("I am a webform page.<br/>"); } %> <br /> <%= GetServerTime() %> <br /> <asp:TextBox ID="txtDateTime" runat="server"></asp:TextBox> <asp:Button ID="btnGetTime" runat="server" Text="獲取時間" onclick="btnGetTime_Click" /> <br /> <% GetDllInfo(); %> </div> </form> </body> </html>
(2)Index所對應的後臺代碼以下:
namespace PageLifeCycleDemo { public partial class Index : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { } protected string GetServerTime() { string result = "服務器時間:" + DateTime.Now.ToString(); return result; } protected void GetDllInfo() { Response.Write("頁面類名稱:" + this.GetType() + "<br/>"); Response.Write("程序集地址:" + this.GetType().Assembly.Location + "<br/>"); Response.Write("父類的名稱:" + this.GetType().BaseType + "<br/>"); Response.Write("程序集地址:" + this.GetType().BaseType.Assembly.Location + "<br/>"); } protected void btnGetTime_Click(object sender, EventArgs e) { txtDateTime.Text = DateTime.Now.ToString(); } } }
這裏,咱們來重點關注一下這個方法:咱們能夠經過寫入如下代碼,而後在aspx中<% GetDllInfo(); %>調用,它顯示了咱們這個ASP.NET項目所屬的程序集在哪一個位置?
protected void GetDllInfo() { Response.Write("頁面類名稱:"+this.GetType() + "<br/>"); Response.Write("程序集地址:"+this.GetType().Assembly.Location + "<br/>"); Response.Write("父類的名稱:"+this.GetType().BaseType + "<br/>"); Response.Write("程序集地址:"+this.GetType().BaseType.Assembly.Location + "<br/>"); }
瀏覽頁面,會顯示如下結果:經過下圖能夠看到,咱們的Index這個頁面會生成一個ASP.index_aspx的類,其父類是Index。
經過上面顯示的路徑找到dll,並拖到反編譯工具(ILSpy或者Reflector,前者開源免費,後者已經收費,但天朝,你懂的。)進行查看。經過下圖能夠看出,頁面類aspx是後臺代碼類所綁定的子類,它的名稱是aspx文件名加上「_aspx」後綴。所以,這裏也就解釋了爲何在aspx中要訪問的方法必須是public和protected的訪問修飾符才能夠。
從上面能夠看出,頁面類繼承自後置代碼類,然後置代碼類又繼承自Page類。咱們從上一篇管道能夠知道,在請求處理管道的第8個事件中建立了Page類對象,那麼咱們去看看Page類。
Page類繼承自TemplateControl,顧名思義,Page類是否就是一個模板控件呢?再看看TemplateControl類:
果不其然,其父類是Control類,Page就是一個封裝過的大控件!那麼,咱們在Page中拖的那些runat="server"的服務器控件,又是保存在哪裏的呢?
原來,在Control父類中,有一個Controls的屬性,它是一個控件的集合:Page中的全部控件,都會存在於這個集合中。
從上一篇請求處理管道中,咱們知道在第11和第12個事件之間會調用Page類對象的ProcessRequest方法進入頁面生命週期。那麼咱們來看看這個ProcessRequest方法:
從圖中能夠看出,這個方法中首先經過調用頁面類對象(咱們請求的頁面都是繼承於Page類的)重寫的FrameworkInitialize方法開始咱們常常聽到的構造控件樹的過程。下面咱們轉到index_aspx這個頁面類重寫的FrameworkInitialize方法中取看看是不是進行了構造頁面控件樹的操做:
看到這裏,咱們不禁地想問,什麼是頁面控件樹?在一個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的子節點,造成一個層級關係。
如今從新回到Page類的ProcessRequest方法中,在建立頁面控件樹完成以後,開始進入一個ProcessRequestMain方法,這個方法則真正地開啓了頁面生命週期之門。
private void ProcessRequest(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) { ...... this.ProcessRequestMain(includeStagesBeforeAsyncPoint, includeStagesAfterAsyncPoint); ...... }
咱們常常在Page_Load方法中使用Page.IsPostBack屬性來判斷請求是不是回發,那麼它是在哪裏設置的呢?原來,在ProcessRequestMain方法中:
接下來就是初始化操做了,初始化操做分爲了三個階段:預初始化、初始化(使用遞歸方式)、初始化完成。
private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) { ...... this.PerformPreInit(); ...... this.InitRecursive(); ...... this.OnInitComplete(); ...... }
預初始化主要利用App_Themes目錄中的內容進行初始化主題,並應用模板頁。
這裏咱們主要看看初始化操做,經過查看源代碼,能夠看出,該方法經過遞歸調用子控件的初始化方法,完成了控件集合中全部控件的初始化操做。
internal virtual void InitRecursive(Control namingContainer) { ...... int count = this._controls.Count; for (int i = 0; i < count; i++) { Control control = this._controls[i]; control.UpdateNamingContainer(namingContainer); if (((control._id == null) && (namingContainer != null)) && !control.flags[0x40]) { control.GenerateAutomaticID(); } control._page = this.Page; control.InitRecursive(namingContainer); } ...... }
再看看初始化方法中都作了哪些初始化操做,細細一看,原來就是爲其動態地生成一個ID(control.GenerateAutomaticID()),而後將該控件的page指針指向當前Page頁等。PreLoad 預加載在 Load 事件以前對頁或控件執行處理,
初始化完成以後,ASP.NET會經過IsPostBack判斷是不是第一次請求,若是不是,那麼首先會加載ViewState並對回發的數據進行處理。
private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) { if(this.IsPostBack) { ...... this.LoadAllState(); ...... this.ProcessPostData(this._requestValueCollection, true); ...... } }
至於ViewState是什麼?又不瞭解的朋友,能夠瀏覽個人另外一篇博文:ASP.NET WebForm溫故知新:ViewState,這裏就再也不贅述。這裏LoadAllState方法主要是將隱藏域中的_VIEWSTATE經過解碼獲取控件的狀態與數據信息,而ProcessPostData方法則是進行了兩個部分的操做:一是將剛剛獲取到的各個控件的狀態與數據信息填充到頁面控件樹中所對應的各個控件中去,二是對比控件狀態是否發生了改變?好比被點擊了?被觸發了某個事件(例如TextChanged、SelectedIndexChanged等)?若有觸發事件,則把須要觸發事件的控件放到一個集合當中去。
處理完ViewState後,就開始進行正式地加載操做了,以下代碼所示:
private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) { ...... this.OnPreLoad(EventArgs.Empty); ...... this.LoadRecursive(); ...... }
在正式加載過程當中也分爲了兩個部分,一個是PreLoad預加載,另一個則是重頭戲Load加載(經過方法名能夠推斷,該方法是經過遞歸方式調用加載的)。首先,調用了OnPreLoad方法進行預加載操做,若是咱們須要在 Load 事件以前對頁或控件(這時頁面控件樹已經構造完成)執行處理,就可使用該事件。經過查看源代碼,在PreLoad方法中會遍歷一個PreLoad事件集合(咱們能夠自定義注入咱們想要的事件),而後依次執行委託所持有的事件。
protected virtual void OnPreLoad(EventArgs e) { EventHandler handler = (EventHandler) base.Events[EventPreLoad]; if (handler != null) { handler(this, e); } }
PreLoad以後就是重頭戲,也是咱們最爲熟悉的Load了,在調用LoadRecursive()方法進入Load事件。
internal virtual void LoadRecursive() { if (this._controlState < ControlState.Loaded) { if (this.AdapterInternal != null) { this.AdapterInternal.OnLoad(EventArgs.Empty); } else { this.OnLoad(EventArgs.Empty); } } if (this._controls != null) { string errorMsg = this._controls.SetCollectionReadOnly("Parent_collections_readonly"); int count = this._controls.Count; for (int i = 0; i < count; i++) { this._controls[i].LoadRecursive(); } this._controls.SetCollectionReadOnly(errorMsg); } if (this._controlState < ControlState.Loaded) { this._controlState = ControlState.Loaded; } }
從上面能夠看出:ASP.NET頁面首先調用自身的OnLoad方法以引起自身的Load事件,接着遞歸調用 Contorls 集合中各個控件的OnLoad方法以引起它們的Load事件。那麼,咱們在頁面後置代碼類中常用的Page_Load事件方法是在哪裏調用的呢?相信咱們都有了答案,就在頁面自身的OnLoad方法中。
private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) { if(this.IsPostBack) { ...... this.ProcessPostData(this._leftoverPostData, false); ...... this.RaiseChangedEvents(); ...... this.RaisePostBackEvent(this._requestValueCollection); ...... } }
加載結束後,會經歷第二次的處理回發數據的事件。那麼,咱們不由會問,爲什麼還要第二次進行ProcessPostData方法的調用,咱們剛剛不是都已經對ViewState進行了解碼並對應到了對應控件樹中的控件了嘛?這裏,咱們首先看看下面一段代碼:
protected void Page_Load(object sender, EventArgs e) { if (IsPostBack) { TextBox txtTest = new TextBox(); txtTest.Text = "動態建立的TextBox"; formIndex.Controls.Add(txtTest); } }
假如咱們要在Page_Load事件中動態地爲Form添加一個TextBox控件,那麼以前的頁面控件樹就發生了改變,因此,這裏須要進行第二次的ProcessPostData方法,如今豁然開朗了吧。
在第二次處理回發數據以後,會調用RaiseChangedEvents方法觸發控件狀態改變事件響應方法,例如TextBox_TextChanged、DropDownList_SelectedIndexChanged事件(這些事件中不包括Button_Click這種回發事件)等。查看源代碼,經過遍歷狀態改變了的控件的集合(在第一次進行ProcessPostData時會檢查控件的狀態是否發生了改變,若是改變了就添加到一個集合中)
internal void RaiseChangedEvents() { if (this._changedPostDataConsumers != null) { for (int i = 0; i < this._changedPostDataConsumers.Count; i++) { Control control = (Control) this._changedPostDataConsumers[i]; if (control != null) { IPostBackDataHandler postBackDataHandler = control.PostBackDataHandler; if (((control == null) || control.IsDescendentOf(this)) && ((control != null) && (control.PostBackDataHandler != null))) { postBackDataHandler.RaisePostDataChangedEvent(); } } } } }
在處理完狀態改變事件響應方法後,會調用RaisePostBackEvent方法觸發例如按鈕控件的回發事件,例如Button_Click回發事件。
private void RaisePostBackEvent(NameValueCollection postData) { if (this._registeredControlThatRequireRaiseEvent != null) { this.RaisePostBackEvent(this._registeredControlThatRequireRaiseEvent, null); } else { string str = postData["__EVENTTARGET"]; bool flag = !string.IsNullOrEmpty(str); if (flag || (this.AutoPostBackControl != null)) { Control control = null; if (flag) { control = this.FindControl(str); } if ((control != null) && (control.PostBackEventHandler != null)) { string eventArgument = postData["__EVENTARGUMENT"]; this.RaisePostBackEvent(control.PostBackEventHandler, eventArgument); } } else { this.Validate(); } } }
經過查看代碼,發現經過回傳的表單數據中根據__EVENTTARGET與__EVENTARGUMENT進行事件的觸發。咱們能夠經過查看ASP.NET生成的前端HTML代碼看到這兩個參數:下圖是一個設置爲AutoPostBack的DropDownList控件,能夠發現回發事件都是經過調用_doPostBack這個js代碼進行表單的submit,而表單中最重要的兩個參數就是eventTarget和eventArgument。
經過瀏覽器提供的開發人員工具查看數據請求報文,能夠看到除了提交form中的input外,還提交了ASP.Net WebForm預置的一些隱藏字段,而這些隱藏字段則是WebForm爲咱們提供便利的基礎。好比EventTarget則記錄剛剛提交給服務器的是哪一個服務器控件。
事件觸發完成以後,加載操做就完成了,這時會調用OnLoadComplete方法進行相關的事件,這裏就再也不贅述了。
這一階段就進入了頁面生命週期的尾巴,開始最終頁面的渲染流程:
private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) { ...... this.PreRenderRecursiveInternal(); ...... this.PerformPreRenderComplete(); ...... this.SaveAllState(); ...... this.OnSaveStateComplete(EventArgs.Empty); ...... this.RenderControl(this.CreateHtmlTextWriter(this.Response.Output)); ...... }
這裏咱們主要看看PreRender、SaveState和Render三個事件。
既然已經進入了頁面渲染階段,爲什麼還要有一個PreRender預呈現階段?經過查找資料,咱們發現微軟這麼設計是爲了給開發者提供一個最後一次更改頁面控件狀態或數據的機會,也就說:你能夠再在這裏注入一個邏輯,最後一次改變控件值,或者統一地改變控件狀態爲某個指定狀態。
而後就是SaveState,這個很好理解,也就說:剛剛給了你最後一次更改的機會結束後,我就要保存最終的ViewState了。這裏須要注意的是:服務器在向瀏覽器返回html以前,對ViewState中的內容是進行了Base64編碼的;
最後就是Render,進行最終的頁面呈現了,換句話說:就是拼接造成HTML字符串。在這個階段,Page 對象會遍歷頁面控件樹並在每一個控件上遞歸地調用此方法。全部 ASP.NET Web 服務器控件都有一個用於寫出發送給瀏覽器的控件標記的 Render 方法。經過對源代碼進行追蹤,能夠看到如下代碼:
internal void RenderChildrenInternal(HtmlTextWriter writer, ICollection children) { if ((this.RareFields != null) && (this.RareFields.RenderMethod != null)) { writer.BeginRender(); this.RareFields.RenderMethod(writer, this); writer.EndRender(); } else if (children != null) { foreach (Control control in children) { control.RenderControl(writer); } } }
在Render過程當中,會判斷當前控件是否含有子控件集合,若是有,那麼遍歷各個子控件的Render方法進行HTML的渲染。能夠想象,從頁面控件樹的根節點調用Render方法,會依次遞歸調用其全部子節點的Render方法,從而獲得一個完整的HTML代碼。
那麼,Render方法結束後,生成的HTML代碼保存到了哪裏呢?原來,Render方法的輸出會寫入Page類對象的 Response 屬性的 OutputStream 中,這就是最終的輸出流做爲響應報文經過HTTP協議返回給瀏覽器端了。
自此,狹義上的頁面生命週期就結束了,但廣義上的頁面聲明週期事件還未結束,還會經歷一個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
PS:背景音樂 from 張國榮 電影英雄本色中的插曲 《當年情》