Dino Esposito
Wintellect
2003 年 8 月
適用于:
Microsoft? ASP.NET
摘要:了解為 ASP.NET Web 頁(yè)面建立的事件模型,以及 Web 頁(yè)面轉(zhuǎn)變?yōu)?HTML 過程中的各個(gè)階段。ASP.NET HTTP 運(yùn)行時(shí)負(fù)責(zé)管理對(duì)象管道,這些對(duì)象首先將請(qǐng)求的 URL 轉(zhuǎn)換成 Page 類的具體實(shí)例,然后再將這些實(shí)例轉(zhuǎn)換成純 HTML 文本。本文將探討那些作為頁(yè)面生命周期標(biāo)志的事件,以及控件和頁(yè)面編寫者如何干預(yù)并改變標(biāo)準(zhǔn)行為。(本文包含一些指向英文站點(diǎn)的鏈接。)
目錄
簡(jiǎn)介
真正的 Page 類
頁(yè)面的生命周期
執(zhí)行的各個(gè)階段
小結(jié)
對(duì)由 Microsoft? Internet 信息服務(wù) (IIS) 處理的 Microsoft? ASP.NET 頁(yè)面的每個(gè)請(qǐng)求都會(huì)被移交到 ASP.NET HTTP 管道。HTTP 管道由一系列托管對(duì)象組成,這些托管對(duì)象按順序處理請(qǐng)求,并將 URL 轉(zhuǎn)換為純 HTML 文本。HTTP 管道的入口是 HttpRuntime 類。ASP.NET 結(jié)構(gòu)為輔助進(jìn)程中的每個(gè) AppDomain 創(chuàng)建一個(gè)此類的實(shí)例。(請(qǐng)注意,輔助進(jìn)程為每個(gè)當(dāng)前正在運(yùn)行的 ASP.NET 應(yīng)用程序維護(hù)一個(gè)特定的 AppDomain。)
HttpRuntime 類從內(nèi)部池中獲取 HttpApplication 對(duì)象,并安排此對(duì)象來處理請(qǐng)求。HTTP 應(yīng)用程序管理器完成的主要任務(wù)就是找到將真正處理請(qǐng)求的類。當(dāng)請(qǐng)求 .aspx 資源時(shí),處理程序就是頁(yè)面處理程序,即從 Page 繼承的類的實(shí)例。資源類型和處理程序類型之間的關(guān)聯(lián)關(guān)系存儲(chǔ)在應(yīng)用程序的配置文件中。更確切地說,默認(rèn)的映射集是在 machine.config 文件的 <httpHandlers> 部分定義的。但是,應(yīng)用程序可以在本地的 web.config 文件中自定義自己的 HTTP 處理程序列表。以下這一行代碼就是用來為 .aspx 資源定義 HTTP 處理程序的。
<add verb="*" path="*.aspx" type="System.Web.UI.PageHandlerFactory"/>
擴(kuò)展名可以與處理程序類相關(guān)聯(lián),并且更多是與處理程序工廠類相關(guān)聯(lián)。在所有情況下,負(fù)責(zé)處理請(qǐng)求的 HttpApplication 對(duì)象都會(huì)獲得一個(gè)實(shí)現(xiàn) IHttpHandler 接口的對(duì)象。如果根據(jù) HTTP 處理程序來解析關(guān)聯(lián)的資源/類,則返回的類將直接實(shí)現(xiàn)接口。如果資源被綁定到處理程序工廠,則還需要額外的步驟。處理程序工廠類實(shí)現(xiàn) IHttpHandlerFactory 接口,此接口的 GetHandler 方法將返回一個(gè)基于 IHttpHandler 的對(duì)象。
HTTP 運(yùn)行時(shí)是如何結(jié)束這個(gè)循環(huán)并處理頁(yè)面請(qǐng)求的?ProcessRequest 方法在 IHttpHandler 接口中非常重要。通過對(duì)代表被請(qǐng)求頁(yè)面的對(duì)象調(diào)用此方法,ASP.NET 結(jié)構(gòu)會(huì)啟動(dòng)將生成瀏覽器輸出的進(jìn)程。
真正的 Page 類特定頁(yè)面的 HTTP 處理程序類型取決于 URL。首次調(diào)用 URL 時(shí),將構(gòu)建一個(gè)新的類,這個(gè)類被動(dòng)態(tài)編譯為一個(gè)程序集。檢查 .aspx 資源的分析進(jìn)程的結(jié)果是類的源代碼。該類被定義為命名空間 ASP 的組成部分,并且被賦予了一個(gè)模擬原始 URL 的名稱。例如,如果 URL 的終點(diǎn)是 page.aspx,則類的名稱就是 ASP.Page_aspx。不過,類的名稱可以通過編程方式來控制,方法是在 @Page 指令中設(shè)置 ClassName 屬性。
HTTP 處理程序的基類是 Page。這個(gè)類定義了由所有頁(yè)面處理程序共享的方法和屬性的最小集合。Page 類實(shí)現(xiàn) IHttpHandler 接口。
在很多情況下,實(shí)際處理程序的基類并不是 Page,而是其他的類。例如,如果使用了代碼分離,就會(huì)出現(xiàn)這種情況。代碼分離是一項(xiàng)開 發(fā)技術(shù),它可以將頁(yè)面所需的代碼隔離到單獨(dú)的 C# 和 Microsoft Visual Basic? .NET 類中。頁(yè)面的代碼是一組事件處理程序和輔助方法,這些處理程序和方法真正決定了頁(yè)面的行為。可以使用 <script runat=server> 標(biāo)記對(duì)此代碼進(jìn)行內(nèi)聯(lián)定義,或者將其放置在外部類(代碼分離類)中。代碼分離類是從 Page 繼承并使用額外的方法的類,被指定用作 HTTP 處理程序的基類。
還有一種情況,HTTP 處理程序也不是基于 Page 的,即在應(yīng)用程序配置文件的 <pages> 部分中,包含了 PageBaseType 屬性的重新定義。
<pages PageBaseType="Classes.MyPage, mypage" />
PageBaseType 屬性指明包含頁(yè)面處理程序的基類的類型和程序集。從 Page 導(dǎo)出的這個(gè)類可以自動(dòng)賦予處理程序擴(kuò)展的自定義方法和屬性集。
頁(yè)面的生命周期完全識(shí)別 HTTP 頁(yè)面處理程序類后,ASP.NET 運(yùn)行時(shí)將調(diào)用處理程序的 ProcessRequest 方法來處理請(qǐng)求。通常情況下,無需更改此方法的實(shí)現(xiàn),因?yàn)樗怯?Page 類提供的。
此實(shí)現(xiàn)將從調(diào)用為頁(yè)面構(gòu)建控件樹的 FrameworkInitialize 方法開始。FrameworkInitialize 方法是 TemplateControl 類(Page 本身從此類導(dǎo)出)的一個(gè)受保護(hù)的虛擬成員。所有為 .aspx 資源動(dòng)態(tài)生成的處理程序都將覆蓋 FrameworkInitialize。在此方法中,構(gòu)建了頁(yè)面的整個(gè)控件樹。
接下來,ProcessRequest 使頁(yè)面經(jīng)歷了各個(gè)階段:初始化、加載視圖狀態(tài)信息和回發(fā)數(shù)據(jù)、加載頁(yè)面的用戶代碼以及執(zhí)行回發(fā)服務(wù)器端事件。之后,頁(yè)面進(jìn)入顯示模式:收集更新的視圖狀態(tài),生成 HTML 代碼并隨后將代碼發(fā)送到輸出控制臺(tái)。最后,卸載頁(yè)面,并認(rèn)為請(qǐng)求處理完畢。
在各個(gè)階段中,頁(yè)面會(huì)觸發(fā)少數(shù)幾個(gè)事件,這些事件可以由 Web 控件和用戶定義的代碼截取并進(jìn)行處理。其中的一些事件是嵌入式控件專用的,因此無法在 .aspx 代碼級(jí)進(jìn)行處理。
要處理特定事件的頁(yè)面應(yīng)該明確注冊(cè)一個(gè)適合的處理程序。不過,為了向后兼容早期的 Visual Basic 編程風(fēng)格,ASP.NET 也支持隱式事件掛鉤的形式。默認(rèn)情況下,頁(yè)面會(huì)嘗試將特定的方法名稱與事件相匹配,如果實(shí)現(xiàn)匹配,則認(rèn)為此方法就是匹配事件的處理程序。ASP.NET 提供了六種方法名稱的特定識(shí)別,它們是 Page_Init、Page_Load、Page_DataBind、Page_PreRender 和 Page_Unload。這些方法被認(rèn)為是由 Page 類提供的相應(yīng)事件的處理程序。HTTP 運(yùn)行時(shí)會(huì)自動(dòng)將這些方法綁定到頁(yè)面事件,這樣,開發(fā)人員就不必再編寫所需的粘接代碼了。例如,如果命名為 Page_Load 的方法綁定到頁(yè)面的 Load 事件,則可省去以下代碼。
this.Load += new EventHandler(this.Page_Load);
對(duì)特定名稱的自動(dòng)識(shí)別是由 @Page 指令的 AutoEventWireup 屬性控制的。如果該屬性設(shè)置為 false,則要處理事件的所有應(yīng)用程序都需要明確連接到頁(yè)面事件。不使用自動(dòng)綁定事件的頁(yè)面性能會(huì)稍好一些,因?yàn)椴恍枰~外匹配名稱與事件。請(qǐng)注意,所有 Microsoft Visual Studio? .NET 項(xiàng)目都是在禁用 AutoEventWireup 屬性的情況下創(chuàng)建的。但是,該屬性的默認(rèn)設(shè)置是 true,即 Page_Load 等方法會(huì)被識(shí)別,并被綁定到相關(guān)聯(lián)的事件。
下表中按順序列出了頁(yè)面的執(zhí)行包括的幾個(gè)階段,執(zhí)行的標(biāo)志是一些應(yīng)用程序級(jí)的事件和/或受保護(hù)并可覆蓋的方法。
表 1:ASP.NET 頁(yè)面生命中的關(guān)鍵事件
階段 | 頁(yè)面事件 | 可覆蓋的方法 |
---|---|---|
頁(yè)面初始化 | Init | |
加載視圖狀態(tài) | LoadViewState | |
處理回發(fā)數(shù)據(jù) | 任意實(shí)現(xiàn) IPostBackDataHandler 接口的控件中的 LoadPostData 方法 | |
加載頁(yè)面 | Load | |
回發(fā)更改通知 | 任意實(shí)現(xiàn) IPostBackDataHandler 接口的控件中的 RaisePostDataChangedEvent 方法 | |
處理回發(fā)事件 | 由控件定義的任意回發(fā)事件 | 任意實(shí)現(xiàn) IPostBackDataHandler 接口的控件中的 RaisePostBackEvent 方法 |
頁(yè)面顯示前階段 | PreRender | |
保存視圖狀態(tài) | SaveViewState | |
顯示頁(yè)面 | Render | |
卸載頁(yè)面 | Unload |
以上所列的階段中有些在頁(yè)面級(jí)是不可見的,并且僅對(duì)服務(wù)器控件的編寫者和要?jiǎng)?chuàng)建從 Page 導(dǎo)出的類的開發(fā)人員有意義。Init、Load、PreRender、Unload,再加上由嵌入式控件定義的所有回發(fā)事件,就構(gòu)成了向外發(fā)送頁(yè)面的各個(gè)階段標(biāo)記。
執(zhí)行的各個(gè)階段頁(yè)面生命周期中的第一個(gè)階段是初始化。這個(gè)階段的標(biāo)志是 Init 事件。在成功創(chuàng)建頁(yè)面的控件樹后,將對(duì)應(yīng)用程序觸發(fā)此事件。換句話說,當(dāng) Init 事件發(fā)生時(shí),.aspx 源文件中靜態(tài)聲明的所有控件都已實(shí)例化并采用各自的默認(rèn)值。控件可以截取 Init 事件以初始化在傳入的 Web 請(qǐng)求的生命周期內(nèi)所需的所有設(shè)置。例如,這時(shí)控件可以加載外部模板文件或設(shè)置事件的處理程序。請(qǐng)注意,這時(shí)視圖狀態(tài)信息尚不可用。
初始化之后,頁(yè)面框架將加載頁(yè)面的視圖狀態(tài)。視圖狀態(tài)是名稱/值對(duì)的集合,在此集合中,控件和頁(yè)面本身存儲(chǔ)了對(duì)所有 Web 請(qǐng)求都必須始終有效的全部信息。視圖狀態(tài)代表了頁(yè)面的調(diào)用上下文。通常,它包含上次在服務(wù)器上處理頁(yè)面時(shí)控件的狀態(tài)。首次在會(huì)話中請(qǐng)求頁(yè)面時(shí),視圖狀態(tài)為 空。默認(rèn)情況下,視圖狀態(tài)存儲(chǔ)在靜默添加到頁(yè)面的隱藏字段中,該字段的名稱是 __VIEWSTATE。通過覆蓋 LoadViewState 方法(Control 類的受保護(hù)、可覆蓋方法),組件開發(fā)人員可以控制視圖狀態(tài)的存儲(chǔ)方式以及視圖狀態(tài)的內(nèi)容映射到內(nèi)部狀態(tài)的方式。
有些方法(如 LoadPageStateFromPersistenceMedium 以及其對(duì)應(yīng)的 SavePageStateToPersistenceMedium),可以用來將視圖狀態(tài)加載并保存到其他存儲(chǔ)介質(zhì)(例如會(huì)話、數(shù)據(jù)庫(kù)或服務(wù)器端文件)中。與 LoadViewState 不同,上述方法只能在從 Page 導(dǎo)出的類中使用。
存儲(chǔ)視圖狀態(tài)之后,頁(yè)面樹中控件的狀態(tài)與頁(yè)面最后一次顯示在瀏覽器中的狀態(tài)相同。下一步是更新它們的狀態(tài)以加入客戶端的更改。處理回發(fā)數(shù)據(jù)階段使控件有機(jī)會(huì)更新其狀態(tài),從而準(zhǔn)確反映客戶端相應(yīng)的 HTML 元素的狀態(tài)。例如,服務(wù)器的 TextBox 控件對(duì)應(yīng)的 HTML 元素是 <input type=text>。在回發(fā)數(shù)據(jù)階段,TextBox 控件將檢索 <input> 標(biāo)記的當(dāng)前值,并使用該值來刷新自己內(nèi)部的狀態(tài)。每個(gè)控件都要從回發(fā)的數(shù)據(jù)中提取值并更新自己的部分屬性。TextBox 控件將更新它的 Text 屬性,而 CheckBox 控件將刷新它的 Checked 屬性。服務(wù)器控件和 HTML 元素的對(duì)應(yīng)關(guān)系可以通過二者的 ID 找到。
在處理回發(fā)數(shù)據(jù)階段的最后,頁(yè)面中的所有控件的狀態(tài)都將使用客戶端輸入的更改來更新前一狀態(tài)。這時(shí),將對(duì)頁(yè)面觸發(fā) Load 事件。
頁(yè)面中可能會(huì)有一些控件,當(dāng)其某個(gè)敏感屬性在兩個(gè)不同的請(qǐng)求中被修改時(shí),需要完成特定的任務(wù)。例如,如果 TextBox 控件的文本在客戶端被修改,則此控件將觸發(fā) TextChanged 事件。每個(gè)控件在其一個(gè)或多個(gè)屬性被修改為客戶端輸入的值時(shí)都可以決定觸發(fā)相應(yīng)的事件。對(duì)于這些更改對(duì)其非常關(guān)鍵的控件,控件實(shí)現(xiàn) IPostBackDataHandler 接口,此接口的 LoadPostData 方法是在 Load 事件后立即調(diào)用的。通過對(duì) LoadPostData 方法進(jìn)行編碼,控件將驗(yàn)證自上次請(qǐng)求后是否發(fā)生了關(guān)鍵更改,并觸發(fā)自己的更改事件。
頁(yè)面生命周期中的關(guān)鍵事件是被調(diào)用以執(zhí)行服務(wù)器端代碼的事件,此代碼與客戶端觸發(fā)的事件相關(guān)聯(lián)。當(dāng)用戶單擊按鈕時(shí),將回發(fā)頁(yè)面。回發(fā)值的集合中包括啟動(dòng)整個(gè)操作的按鈕的 ID。如果控件實(shí)現(xiàn) IPostBackEventHandler 接口(如按鈕和鏈接按鈕),頁(yè)面框架將調(diào)用 RaisePostBackEvent 方法。此方法的行為取決于控件的類型。就按鈕和鏈接按鈕而言,此方法將查找 Click 事件處理程序并運(yùn)行相關(guān)的委托。
處理完回發(fā)事件之后,頁(yè)面就可以顯示了。這個(gè)階段的標(biāo)志是 PreRender 事件。控件可以利用這段時(shí)間來執(zhí)行那些需要在保存視圖狀態(tài)和顯示輸出的前一刻執(zhí)行的更新操作。下一個(gè)狀態(tài)是 SaveViewState,在此狀態(tài)中,所有控件和頁(yè)面本身都將更新自己 ViewState 集合的內(nèi)容。然后,將得到序列化、散列、Base64 編碼的視圖狀態(tài),而且此視圖狀態(tài)與隱藏字段 __VIEWSTATE 相關(guān)聯(lián)。
通過覆蓋 Render 方法可以改變各個(gè)控件的顯示機(jī)制。此方法接受 HTML 書寫器對(duì)象,并使用此對(duì)象來積累所有要為控件生成的 HTML 文本。Page 類的 Render 方法的默認(rèn)實(shí)現(xiàn)包括對(duì)所有成員控件的遞歸調(diào)用。對(duì)于每個(gè)控件,頁(yè)面都將調(diào)用 Render 方法,并緩存 HTML 輸出。
頁(yè)面生命中的最后一個(gè)標(biāo)志是 Unload 事件,在頁(yè)面對(duì)象消除之前發(fā)生。在此事件中,您應(yīng)該釋放所有可能占用的關(guān)鍵資源(例如文件、圖形對(duì)象、數(shù)據(jù)庫(kù)連接等)。
在此事件之后,也就是最后,瀏覽器接收 HTTP 響應(yīng)數(shù)據(jù)包并顯示頁(yè)面。
小結(jié)ASP.NET 頁(yè)面對(duì)象模型因其事件機(jī)制而顯得格外新穎獨(dú)特。Web 頁(yè)面由控件組成,這些控件既可以產(chǎn)生豐富的基于 HTML 的用戶界面,又可以通過事件與用戶交互。以前,在 Web 應(yīng)用程序的上下文中設(shè)置事件模型是件有挑戰(zhàn)性的工作。可我們驚奇的看到,客戶端生成的事件可以由服務(wù)器端的代碼來解決,而且只進(jìn)行一些相應(yīng)的修改后,此過 程仍可以輸出相同的 HTML 頁(yè)面。
掌握這個(gè)模型對(duì)于了解頁(yè)面生命周期的各個(gè)階段,以及頁(yè)面對(duì)象如何被 HTTP 運(yùn)行時(shí)實(shí)例化并使用是非常重要的。
關(guān)于作者
Dino Esposito 是一位來自意大利羅馬的培訓(xùn)教師和顧問。作為 Wintellect 團(tuán)隊(duì)的成員,Dino 專門研究 ASP.NET 和 ADO.NET,主要在歐洲和美國(guó)從事教學(xué)和咨詢工作。此外,Dino 還負(fù)責(zé)管理 Wintellect 的 ADO.NET 課件,并為 MSDN 期刊的“Cutting Edge”專欄撰寫文章。要與他聯(lián)系,請(qǐng)向 dinoe@wintellect.com 發(fā)送電子郵件。
發(fā)表于 2003年11月4日 8:18