一、伺服器腳本基礎介紹
首先,我們先複習一下Web伺服器頁面的基本執行方式:
1、客戶端透過在瀏覽器的網址列敲入位址來傳送請求到伺服器端
2、伺服器接收到請求之後,發給對應的伺服器端頁面(也就是腳本)來執行,腳本產生客戶端的回應,發送回客戶端
3、客戶端瀏覽器接收到伺服器傳回的回應,對Html進行解析,將圖形化的網頁呈現在用戶前方
對於伺服器和客戶端的交互,通常透過下面幾種主要方式:
1、Form:這是最主要的方式,標準化的控制項來取得使用者的輸入,Form的提交將資料傳送給伺服器端處理
2、QueryString:透過在Url後面帶參數達到將參數傳送給伺服器,這種方式其實跟Get方式的Form是一樣的
3、Cookies:這是一種比較特殊的方式,通常用於使用者身分的確認
二、ASP.Net簡介
傳統的伺服器腳本語言,如ASP、JSP等,編寫伺服器腳本的方式大同小異,都是在Html中嵌入解釋或編譯執行的程式碼,由伺服器平台執行這些程式碼來產生Html;對於這類似的腳本,頁面的生存週期其實很簡單,就是從開頭至結尾,執行完所有的程式碼,當然用Java編寫的Servlet可以寫更複雜的程式碼,但從結構上看,跟JSP沒什麼差別。
ASP.Net的出現,打破了這種傳統;ASP.Net採用了CodeBehind技術和伺服器端控件,加入了伺服器端的事件的概念,改變了腳本語言編寫的模式,更加貼近Window編程,使Web編程更加簡單、直覺;但是我們要看到,ASP.Net本身並沒有改變Web程式設計的基本模式,只是封裝了一些細節、提供了一些易用的功能,使程式碼更容易編寫和維護;從某種程度上來說,將伺服器端執行的方式複雜化了,這就是我們今天要討論的主體:ASP.Net Web Page的生存週期。
三、ASP.Net請求處理模式
我們說,ASP.Net的Web Page並沒有脫離Web編程的模式,所以它仍然是以請求->接收請求->處理請求->發送響應這樣的模式在工作,每一次與客戶端的互動都會引發新的請求,所以一個Web Page的生命週期是以一次請求為基礎的。
當IIS收到客戶端的請求的時候,會將請求交給aspnet_wp這個進程來處理,這個進程會查看請求的應用程式域是否存在,如果不存在則會創建一個,然後會創建一個Http運行時(HttpRuntime )來處理請求,這個運行時「為目前應用程式提供一組ASP.NET 運行時服務」(摘自MSDN)。
HttpRuntime在處理請求的時候,會維護一系列的應用程式實例,也就是應用程式的Global類別(global.asax)的實例,這些實例在沒有請求的時候,會存放在一個應用程式集區中(實際上應用程式集區由另一個類別來維護,HttpRuntime只是簡單的呼叫),每接收到一個請求,HttpRuntime都會取得一個閒置的實例來處理請求,這個實例在請求結束前不會處理其他的請求,處理完畢之後,它又會回到池中,「一個實例在其生存期內被用於處理多個請求,但它一次只能處理一個請求。」(摘自MSDN)
當應用程式實例處理請求的時候,它會建立請求頁面類別的實例,執行它的ProcessRequest方法來處理請求,這個方法也就是Web Page生命週期的開始。
四、Aspx頁面與CodeBehind
在深入了解頁面的生命週期之前,我們先來探討一些Aspx與CodeBehind之間的關係。
<%@ Page language="c#" Codebehind="WebForm.aspx.cs" Inherits="MyNamespace.WebForm" %>
相信使用過CodeBehind技術的朋友,對ASPX頂部的這句話應該是非常熟悉了,我們來一項一項的分析它:
Page language="c#" 這個就不用多說了吧
Codebehind="WebForm.aspx.cs" 這句話表示綁定的程式碼檔案
Inherits="MyNamespace.WebForm" 這句話非常重要,它表示頁面繼承的類別名稱,也就是CodeBehind的程式碼檔案中的類,這個類別必須從System.Web.WebControls.Page派生
從上面我們可以分析出,實際上CodeBehind中的類別就是頁面(ASPX)的基類,到這裡,可能有些朋友要問了,在編寫ASPX的時候,完全是按照ASP的方式,在Html中嵌入代碼或嵌入伺服器控件,沒有看到所謂「類」的影子啊?
這個問題其實不複雜,各位使用ASP.Net程式設計的朋友可以到你們的系統碟:WINDOWSMicrosoft.NETFramework<版本號碼>Temporary ASP.NET Files這個目錄下,這個下面就放了所有本機上存在的ASP.Net應用程式的臨時文件,子目錄的名稱就是應用程式的名稱,然後再下去兩層(為了確保唯一,ASP.Net自動產生了兩層子目錄,並且子目錄名稱是隨機的),然後我們會發現有很多類似:「yfy1gjhc.dll」、「xeunj5u3.dll」這樣的連結庫以及「komee-bp.0.cs」、「9falckav.0.cs」這樣的來源文件,其實這就是ASPX被ASP.Net動態編譯後的結果,打開這些原始檔我們可以發現:
public class WebForm_aspx : MyNamespace.WebForm, System.Web.SessionState.IRequiresSessionState
這就印證了我們前面的說法,ASPX是程式碼綁定類別的子類,它的名稱是ASPX檔案名稱加上「_aspx」後綴,透過研究這些程式碼我們可以發現,實際上所有aspx中定義的伺服器控制項都是在這些程式碼中產生的,然後動態產生這些程式碼的時候,把原來在ASPX中嵌入的程式碼寫在了對應的位置。
當某個頁面第一次被存取的時候,Http運行時就會使用一個程式碼產生器去解析ASPX檔案並產生原始程式碼並編譯,然後以後的存取就直接呼叫編譯後的dll,這也是為什麼ASPX第一次訪問的時候非常慢的原因。
解釋了這個問題,我們再來看另一個問題。我們在使用程式碼綁定的時候,在設計頁面拖一個控件,然後切換到程式碼視圖,就可以直接在Page_Load中使用這個控件了,既然控件是在子類中產生的,那為什麼在父類中可以直接使用呢?
實際上我們可以發現,每當用VS.Net拖一個控製到頁面上,代碼綁定文件中總是會類似這樣的添加一個聲明:
protected System.Web.WebControls.Button Button1;
我們可以發現這個字段被聲明成protected,而且名字與ASPX中控制的ID一致,仔細想一想,這個問題就迎刃而解了。我們前面提到ASPX的原始碼是被生成器動態產生和編譯的,生成器會產生動態產生每一個伺服器控件的程式碼,在產生的時候,它會檢查父類有沒有聲明這個控件,如果聲明了,它會加入類似下面的一句程式碼:
this.DataGrid1 = __ctrl;
這個__ctrl就是產生該控制項的變量,這時候它就把控制項的引用賦給了父類中對應的變量,這也是為什麼父類中的聲明必須為protected(實際上也可以為public),因為要保證子類別能夠呼叫。
然後在執行Page_Load的時候,因為這時候父類的聲明已經被子類中的初始化代碼賦了值,所以我們就可以使用這個字段來訪問對應的控件,了解了這些,我們就不會犯在代碼綁定文件中的構造器裡使用控件,造成空引用的異常的錯誤了,因為構造器是最先執行的,這時候子類的初始化還沒有開始,所以父類中的字段是空值,至於子類別是什麼時候初始化我們放到後面討論。
五、頁面生存週期
現在回到第三個標題中講到的內容,我們講到了HttpApplication的實例接收請求,並創建頁面類別的實例,實際上這個實例也就是動態編譯的ASPX的類別的一個實例,上一個標題中我們了解到ASPX實際上是程式碼綁定中類別的子類,所以它繼承了所有的protected方法。
現在我們來看看VS.Net自動產生的CodeBehind類別的程式碼,以此來開始我們對頁面生命週期的探討:
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN:此呼叫是ASP.NET Web 窗體設計器所必需的。
//
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// 設計器支援所需的方法- 不要使用程式碼編輯器修改
/// 此方法的內容。
/// </summary>
private void InitializeComponent()
{
this.DataGrid1.ItemDataBound += new System.Web.UI.WebControls.DataGridItemEventHandler(this.DataGrid1_ItemDataBound);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
這個就是使用VS.Net產生的Page的程式碼,我們來看,這裡面有兩個方法,一個是OnInit,一個是InitializeComponent,後者被前者調用,實際上這就是頁面初始化的開始,在InitializeComponent中我們看到了控制項的事件宣告和Page的Load宣告。
以下是從MSDN中摘錄的一段描述和一個頁面生命週期方法和事件觸發的順序表:
“每次請求ASP.NET 頁時,伺服器就會載入一個ASP.NET 頁,並在請求完成時卸載該頁。 。
框架可使執行狀態管理相對容易一些,但是為了獲得連續性效果,控制項開發人員必須知道控制項的執行順序。 、控制項呈現時處於哪種狀態。的連結。
階段 | 控制項需要執行的操作 | 要重寫的方法或事件 |
初始化 | 初始化在傳入Web 請求生命週期內所需的設定。請參閱處理繼承的事件。 | Init 事件(OnInit 方法) |
載入視圖狀態 | 在此階段結束時,就會自動填入控制項的ViewState 屬性,詳見維護控制項中的狀態中的介紹。控制項可以重寫LoadViewState 方法的預設實現,以自訂狀態還原。 | LoadViewState 方法 |
處理回發資料 | 處理傳入窗體數據,並相應地更新屬性。請參閱處理回發資料。 注意只有處理回發資料的控制項參與此階段。 | LoadPostData 方法(如果已實作IPostBackDataHandler) |
載入 | 執行所有請求共有的操作,如設定資料庫查詢。此時,樹中的伺服器控制項已建立並初始化、狀態已還原且窗體控制項反映了客戶端的資料。請參閱處理繼承的事件。 | Load 事件(OnLoad 方法)會 |
傳送回發變更通知 | 引發變更事件以回應目前和先前回發之間的狀態變更。請參閱處理回發資料。 注意只有引發回發變更事件的控制項參與此階段。 | RaisePostDataChangedEvent 方法(如果已實作IPostBackDataHandler) |
處理回發事件 | 處理引起回發的客戶端事件,並在伺服器上引發對應的事件。請參閱捕獲回發事件。 注意只有處理回發事件的控制項參與此階段。 | RaisePostBackEvent 方法(如果已實作IPostBackEventHandler) |
預先呈現 | 在呈現輸出之前執行任何更新。可以儲存在預呈現階段對控制項狀態所做的更改,而在呈現階段所對的變更則會遺失。請參閱處理繼承的事件。 | PreRender 事件(OnPreRender 方法) |
儲存狀態 | 在此階段後,自動將控制項的ViewState 屬性保持到字串物件中。此字串物件被傳送到客戶端並作為隱藏變數發送回來。為了提高效率,控制項可以重寫SaveViewState 方法以修改ViewState 屬性。請參閱維護控制項中的狀態。 | SaveViewState 方法 |
呈現 | 產生呈現給客戶端的輸出。請參閱呈現ASP.NET 伺服器控制項。 | Render 方法 |
處置 | 執行銷毀控制項前的所有最終清理操作。在此階段必須釋放對昂貴資源的引用,例如資料庫連結。請參閱ASP.NET 伺服器控制項中的方法。 | Dispose 方法 |
卸載 | 執行銷毀控制項前的所有最終清理操作。控制項作者通常在Dispose 中執行清除,而不處理此事件。 | UnLoad 事件(On UnLoad 方法) |
從這個表裡面我們可以清楚的看到一個Page從裝載到卸載之間調用的方法和觸發的時間,接下來我們就深入的對其進行一些分析。
看了上面的表,細心的朋友可能要問了,既然OnInit是頁面生命週期的開始,而我們在上一講中談到控件在子類中被創建,那麼在這裡實際上在InitializeComponent方法中我們已經可以使用父類別中聲名的欄位了,那麼就代表子類別的初始化更在這之前?
在第三個標題中我們講到了頁面類別的ProcessRequest才是真正意義上的頁面聲明周期的開始,這個方法是由HttpApplication調用的(其中調用的方式比較複雜,有機會單獨撰文來講解),一個Page請求的處理就是從這個方法開始,透過反編譯.Net類別庫來查看原始程式碼,我們發現在System.Web.WebControls.Page的基類:System.Web.WebControls.TemplateControl(它是頁面和使用者控制項的基底類別)中定義了一個「FrameworkInitialize」虛擬方法,然後在Page的ProcessRequest中最先呼叫了這個方法,在生成器產生的ASPX的源代碼中我們發現了這個方法的踪影,所有的控制項都在這個方法中被初始化,頁面的控制樹就在這個時候產生。
接下來的事情就簡單了,我們來逐步分析頁面生命週期的每一項:
1、初始化
初始化對應Page的Init事件和OnInit方法。
如果要重寫,MSDN推薦的方式是重載OnInti方法,而不是增加一個Init事件的代理,這兩者是有差別的,前者可以控制調用父類OnInit方法的順序,而後者只能在父類的OnInit後執行(實際上是在OnInit裡面被呼叫的)。
2、 載入視圖狀態
這是個比較重要的方法,我們知道,對於每次請求,實際上是由不同的頁面類別實例來處理的,為了確保兩次請求間的狀態,ASP.Net使用了ViewState。
LoadViewState方法就是從ViewState中取得上一次的狀態,並且依照頁面的控制項樹的結構,用遞歸來遍歷整個樹,將對應的狀態還原到每個控制項上。
3. 處理回發資料
這個方法是用來檢查客戶端發回的控制項資料的狀態是否改變了。方法的原型:
public virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection)
postDataKey是標識控制項的關鍵字(也就是postCollection中的Key),postCollection是包含回發資料的集合,我們可以重寫這個方法,然後檢查回發的資料是否發生了變化,如果是則傳回一個True,「如果控制項狀態因回發而更改,則LoadPostData 傳回true;否則傳回false。頁框追蹤所有傳回true 的控制項並在這些控制項上呼叫RaisePostDataChangedEvent。 」(摘自MSDN)
這個方法是System.Web.WebControls.Control中定義的,也是所有需要處理事件的自訂控制項需要處理的方法,對於我們今天討論的Page來說,可以不用管它。
4. 載入
載入對應Load事件和OnLoad方法,對於這個事件,相信大多數朋友都會比較熟悉,用VS.Net生成的頁面中的Page_Load方法就是響應Load事件的方法,對於每一次請求,Load事件都會觸發,Page_Load方法也會執行,相信這也是大多數人了解ASP.Net的第一步。
Page_Load方法回應了Load事件,這個事件是在System.Web.WebControl.Control類別中定義的(這個類別是Page和所有伺服器控制項的祖宗),並且在OnLoad方法中被觸發。
很多人可能碰到過這樣的事情,寫了一個PageBase類,然後在Page_Load中來驗證用戶信息,結果發現不管驗證是否成功,子類頁面的Page_Load總是會先執行,這個時候很可能留下一些安全性的隱患,使用者可能在沒有得到驗證的情況下就執行了子類別中的Page_Load方法。
出現這個問題的原因很簡單,因為Page_Load方法是在OnInit中被加入到Load事件中的,而子類別的OnInit方法中是先加入了Load事件,然後再呼叫base.OnInit,這樣就造成了子類的Page_Load先被加,那麼先執行了。
要解決這個問題也很簡單,有兩種方法:
1) 在PageBase中重載OnLoad方法,然後在OnLoad中驗證用戶,然後調用base.OnLoad,因為Load事件是在OnLoad中觸發,這樣我們就可以保證在觸發Load事件之前驗證使用者。
2) 在子類別的OnInit方法中先呼叫base.OnInit,這樣來確保父類別先執行Page_Load
5、 發送回發更改通知
這個方法對應第3步的處理回發數據,如果處理回發數據返回True,頁面框架就會呼叫此方法來觸發資料變更的事件,所以自訂控制項的回發資料變更事件需要在此方法中觸發。
同樣這個方法對於Page來說,沒有太大的用處,當然你也可以在Page的基礎上自己定義資料變更的事件,這當然也是可以的。
6. 處理回發事件
這個方法是大多數伺服器控制項事件引發的地方,當請求中包含控制項事件觸發的資訊時(伺服器控制項的事件是另一個論題,我會在不久將來另外撰文討論),頁面控件會呼叫對應控制項的RaisePostBackEvent方法來引發伺服器端的事件。
這裡又引出一個常見的問題:
經常有網友問,為什麼修改提交後的資料並沒有更改
多數的情況都是他們沒有理解伺服器事件的觸發流程,我們可以看出,觸發伺服器事件是在Page的Load之後,也就是說頁面會先執行Page_Load,然後才會執行按鈕(這裡以按鈕為例)的點擊事件,很多朋友都是在Page_Load中綁定數據,然後在按鈕事件中處理更改,這樣做有一個毛病,Page_Load永遠都是在按鈕事件之前執行,那麼意味著資料還來不及更改,Page_Load中的資料綁定的程式碼就先執行了,原有的資料還來不及了控件,那麼執行按鈕事件的時候,實際上獲得的是原有的數據,那麼更新當然就沒有效果了。
更改這個問題也非常簡單,比較合理的做法是把資料綁定的程式碼寫成一個方法,我們假設為BindData:
private void BindData()
{
//綁定數據
}
然後修改PageLoad:
private void Page_Load( object sender,EventArgs e )
{
if( !IsPostBack )
{
BindData(); //在頁面第一次存取的時候綁定資料}
}
最後在按鈕事件中:
private Button1_Click( object sender,EventArgs e )
{
//更新資料BindData();//重新綁定數據
}
7.預呈現
最終請求的處理都會轉變為發回伺服器的回應,預呈現這個階段就是執行在最終呈現之前所作的狀態的更改,因為在呈現一個控制項之前,我們必鬚根據它的屬性來產生Html ,例如Style屬性,這是最典型的例子,在預先呈現之前,我們可以更改一個控制項的Style,當執行預先呈現的時候,我們就可以把Style儲存下來,作為呈現階段顯示Html的樣式資訊。
8.保存狀態
這個階段是針對載入狀態的,我們多次提到,請求之間是不同的實例在處理,所以我們需要把本次的頁面和控制項的狀態保存起來,這個階段就是把狀態寫入ViewState的階段。
9.呈現
到這裡,實際上頁面對請求的處理基本上就告一段落了,在Render方法中,會遞歸整個頁面的控制樹,依序呼叫Render方法,把對應的Html程式碼寫入最終回應的流中。
10.處置
其實就是Dispose方法,在這個階段會釋放佔用的資源,例如資料庫連線。
11.卸載
最後,頁面會執行OnUnLoad方法觸發UnLoad事件,處理在頁面物件被銷毀之前的最後處理,實際上ASP.Net提供這個事件只是設計上的考慮,通常資源的釋放都會在Dispose方法中完成,所以這個方法也變成雞肋了。
我們簡單的介紹了頁面的生存週期,對於伺服器端事件的處理做了不太深入的講解,今天主要是想大家了解頁面執行的周期,對於伺服器控制項的事件和生存期我會在後續在寫一些文章來探討。
這些內容是我在學習ASP.Net的時候對Page研究的一些心得,具體的細節沒有很詳細的探討,更多的內容請大家參考MSDN,但是我舉了一些初學者常犯的錯誤和出現錯誤的原因,希望可以帶給大家啟發。