天天看點

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,但是我舉了一些初學者常犯的錯誤和出現錯誤的原因,希望可以給大家帶來啟發。