閱讀822 返回首頁    go 阿裏雲 go 技術社區[雲棲]


ASP.NET Web Page應用深入探討

一、服務器腳本基礎介紹

首先,我們先複習一下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.NetWebPage的生存周期。

三、ASP.Net請求處理模式

我們說,ASP.Net的WebPage並沒有脫離Web編程的模式,所以它仍然是以請求->接收請求->處理請求->發送響應這樣的模式在工作,每一次與客戶端的交互都會引發一次新的請求,所以一個WebPage的生命周期是以一次請求為基礎的。

當IIS收到客戶端的請求的時候,會將請求交給aspnet_wp這個進程來處理,這個進程會查看請求的應用程序域是否存在,如果不存在則會創建一個,然後會創建一個Http運行時(HttpRuntime)來處理請求,這個運行時“為當前應用程序提供一組ASP.NET運行時服務”(摘自MSDN)。

HttpRuntime在處理請求的時候,會維護一係列的應用程序實例,也就是應用程序的Global類(global.asax)的實例,這些實例在沒有請求的時候,會存放在一個應用程序池中(實際上應用程序池由另一個類來維護,HttpRuntime隻是簡單的調用),每接收到一個請求,HttpRuntime都會獲取一個閑置的實例來處理請求,這個實例在請求結束前不會處理其他的請求,處理完畢之後,它又會回到池中,“一個實例在其生存期內被用於處理多個請求,但它一次隻能處理一個請求。”(摘自MSDN)

當應用程序實例處理請求的時候,它會創建請求頁麵類的實例,執行它的ProcessRequest方法來處理請求,這個方法也就是WebPage生命周期的開始。

四、Aspx頁麵與CodeBehind

在深入了解頁麵的生命周期之前,我們先來探討一些Aspx與CodeBehind之間的關係。

<%@Pagelanguage="c#"Codebehind="WebForm.aspx.cs"Inherits="MyNamespace.WebForm"%>

相信使用過CodeBehind技術的朋友,對ASPX頂部的這句話應該是非常熟悉了,我們來一項一項的分析它:

Pagelanguage="c#"這個就不用多說了吧

Codebehind="WebForm.aspx.cs"這一句表示綁定的代碼文件

Inherits="MyNamespace.WebForm"這句非常重要,它表示頁麵繼承的類名稱,也就是CodeBehind的代碼文件中的類,這個類必須從System.Web.WebControls.Page派生

從上麵我們可以分析出,實際上CodeBehind中的類就是頁麵(ASPX)的基類,到這裏,可能有些朋友要問了,在編寫ASPX的時候,完全是按照ASP的方式,在Html中嵌入代碼或者嵌入服務器控件,沒有看到所謂“類”的影子啊?

這個問題實際上並不複雜,各位使用ASP.Net編程的朋友可以到你們的係統盤:/WINDOWS/Microsoft.NET/Framework/<版本號>/TemporaryASP.NETFiles這個目錄下,這個下麵就放了所有本機上存在的ASP.Net應用程序的臨時文件,子目錄的名稱就是應用程序的名稱,然後再下去兩層(為了保證唯一,ASP.Net自動產生了兩層子目錄,並且子目錄名稱是隨機的),然後我們會發現有很多類似:“yfy1gjhc.dll”、“xeunj5u3.dll”這樣的鏈接庫以及“komee-bp.0.cs”、“9falckav.0.cs”這樣的源文件,實際上這就是ASPX被ASP.Net動態編譯後的結果,打開這些源文件我們可以發現:

publicclassWebForm_aspx:MyNamespace.WebForm,System.Web.SessionState.IRequiresSessionState

這就印證了我們前麵的說法,ASPX是代碼綁定類的子類,它的名稱是ASPX文件名加上“_aspx”後綴,通過研究這些代碼我們可以發現,實際上所有aspx中定義的服務器控件都是在這些代碼中生成的,然後動態產生這些代碼的時候,把原來在ASPX中嵌入的代碼寫在了相應的位置。

當某個頁麵第一次被訪問的時候,Http運行時就會使用一個代碼生成器去解析ASPX文件並生成源代碼並編譯,然後以後的訪問就直接調用編譯後的dll,這也是為什麼ASPX第一次訪問的時候非常慢的原因。

解釋了這個問題,我們再來看另一個問題。我們在使用代碼綁定的時候,在設計頁麵拖一個控件,然後切換到代碼視圖,就可以直接在Page_Load中使用這個控件了,既然控件是在子類中產生的,那為什麼在父類中可以直接使用呢?

實際上我們可以發現,每當用VS.Net拖一個控件到頁麵上,代碼綁定文件中總是會類似這樣的添加一個聲明:

protectedSystem.Web.WebControls.ButtonButton1;

我們可以發現這個字段被聲明成protected,而且名字與ASPX中控件的ID一致,仔細想一想,這個問題就迎刃而解了。我們前麵提到ASPX的源代碼是被生成器動態生成和編譯的,生成器會產生動態生成每一個服務器控件的代碼,在生成的時候,它會檢查父類有沒有聲明這個控件,如果聲明了,它會添加類似下麵的一句代碼:

this.DataGrid1=__ctrl;

這個__ctrl就是生成該控件的變量,這時候它就把控件的引用賦給了父類中相應的變量,這也是為什麼父類中的聲明必須為protected(實際上也可以為public),因為要保證子類能夠調用。

然後在執行Page_Load的時候,因為這時候父類的聲明已經被子類中的初始化代碼賦了值,所以我們就可以使用這個字段來訪問對應的控件,了解了這些,我們就不會犯在代碼綁定文件中的構造器裏使用控件,造成空引用的異常的錯誤了,因為構造器是最先執行的,這時候子類的初始化還沒有開始,所以父類中的字段是空值,至於子類是什麼時候初始化我們放到後麵討論。

五、頁麵生存周期

現在回到第三個標題中講到的內容,我們講到了HttpApplication的實例接收請求,並創建頁麵類的實例,實際上這個實例也就是動態編譯的ASPX的類的一個實例,上一個標題中我們了解到ASPX實際上是代碼綁定中類的子類,所以它繼承了所有的protected方法。

現在我們來看看VS.Net自動生成的CodeBehind類的代碼,以此來開始我們對頁麵生命周期的探討:

#regionWebFormDesignergeneratedcode

overrideprotectedvoidOnInit(EventArgse)
{
 //
 //CODEGEN:該調用是ASP.NETWeb窗體設計器所必需的。
 //
 InitializeComponent();
 base.OnInit(e);
}

///<summary>
///設計器支持所需的方法-不要使用代碼編輯器修改
///此方法的內容。
///</summary>

privatevoidInitializeComponent()
{
 this.DataGrid1.ItemDataBound+=newSystem.Web.UI.WebControls.DataGridItemEventHandler(this.DataGrid1_ItemDataBound);

 this.Load+=newSystem.EventHandler(this.Page_Load);
}

#endregion

這個就是使用VS.Net產生的Page的代碼,我們來看,這裏麵有兩個方法,一個是OnInit,一個是InitializeComponent,後者被前者調用,實際上這就是頁麵初始化的開始,在InitializeComponent中我們看到了控件的事件聲明和Page的Load聲明。

下麵是從MSDN中摘錄的一段描述和一個頁麵生命周期方法和事件觸發的順序表:

“每次請求ASP.NET頁時,服務器就會加載一個ASP.NET頁,並在請求完成時卸載該頁。頁及其包含的服務器控件負責執行請求並將HTML呈現給客戶端。雖然客戶端和服務器之間的通訊是無狀態的和斷續的,但是必須使客戶感覺到這是一個連續執行的過程。”

“這種連續性假象是由ASP.NET頁框架、頁及其控件實現的。回發後,控件的行為必須看起來是從上次Web請求結束的地方開始的。雖然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事件(OnUnLoad方法)

從這個表裏麵我們可以清楚的看到一個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、加載視圖狀態

這是個比較重要的方法,我們知道,對於每次請求,實際上是由不同的頁麵類實例來處理的,為了保證blicvirtualboolostDataKey是標識控件的關鍵字(也就是postCollection中的Key),postCollection是包含回發數據的集合,我們可以重寫這個方法,然後檢查回發的數據是否發生了變化,如果是則返回一個True,“如果控件狀態因回發而更改,則LoadPostData返回true;否則返回false。頁框架跟蹤所有返回true的控件並在這些控件上調用RaisePostDataCh個方法是System.Web.WebControls.Control中定義的,也是所有需要處理事件的自定義控件需要處理的方法,對於我們今天討論的Page來說,可以不用管它。

3、處理回發數據

這個方法是用來檢查客戶端發回的控件數據的狀態是否發生了改變。方法的原型:

publicvirtualboolLoadPostData(stringpostDataKey,NameValueCollectionpostCollection)

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:

privatevoidBindData()
{
 //綁定數據
}

然後修改PageLoad:

privatevoidPage_Load(objectsender,EventArgse)
{
 if(!IsPostBack)
 {
BindData();//在頁麵第一次訪問的時候綁定數據
 }
}

最後在按鈕事件中:

privateButton1_Click(objectsender,EventArgse)
{
 //更新數據
 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,但是我舉了一些初學者常犯的錯誤和出現錯誤的原因,希望可以給大家帶來啟發。

最後更新:2017-04-02 00:06:36

  上一篇:go 在ASP.NET中使用SQL的IN操作
  下一篇:go ASP.NET中為DataGrid添加合計字段