2.39k likes | 2.65k Views
第十章 應用程式與狀態保留. 在前面章節中,我們已經能夠處理單一連線與單一網頁的 ASP.NET 程式設計。在本章中,我們將更進一步,撰寫一些跨網頁或跨連線的網頁。 在撰寫跨網頁或跨連線的 ASP.NET 程式前,應該要先了解何謂 ASP.NET 應用程式,以及如何在不同情況下保留或分享狀態以便於應用,保留狀態使用的技術包含工作階段狀態 (SessionState) 、應用程式狀態 (ApplicationState) 、 Cookie 、檢視狀態 (ViewState) 等等,而這些都將在本章中介紹。. 第十章 應用程式與狀態保留.
E N D
第十章應用程式與狀態保留 在前面章節中,我們已經能夠處理單一連線與單一網頁的ASP.NET程式設計。在本章中,我們將更進一步,撰寫一些跨網頁或跨連線的網頁。 在撰寫跨網頁或跨連線的ASP.NET程式前,應該要先了解何謂ASP.NET應用程式,以及如何在不同情況下保留或分享狀態以便於應用,保留狀態使用的技術包含工作階段狀態(SessionState)、應用程式狀態(ApplicationState)、Cookie、檢視狀態(ViewState)等等,而這些都將在本章中介紹。
第十章 應用程式與狀態保留 • 由於ASP.NET採用了事件驅動的方式設計網頁,故保留前一次網頁的狀態需要透過一些技巧才能達成。 • 而傳統ASP所使用的Cookie、Session、Application等內建物件,在ASP.NET也提供,但有些許不同,這些物件能夠讓我們設計跨網頁或跨連線的網頁,我們將於本章中介紹這些保留狀態的機制。 • 而本章內容除了可以應用於各種特殊網頁的設計之外,還包含了許多觀念上的介紹。故在難易度方便比起前面幾章,稍微難了一些,但只有建立正確的觀念,在未來設計實用的網站時,才不至於出現錯誤而找不出原因。
大綱 • 10.1 ASP.NET應用程式(建議與第9章一起教學) • 10.1.1 ASP.NET應用程式概觀 • 10.1.2 ASP.NET的運行 • 10.1.3 Global.asax與HttpApplication • 10.1.4 應用程式層級的錯誤處理 • 10.2 狀態管理 • 10.3 狀態保留的客戶端機制 • 10.3.1 檢視狀態 • 10.3.2 Cookie • 10.4 狀態保留的伺服器端機制 • 10.4.1 工作階段狀態 • 10.4.2 應用程式狀態 • 10.5 本章回顧
10.1 ASP.NET應用程式 • 在第5章中,我們曾經提及ASP.NET的應用程式生命週期,它與ASP.NET網頁的生命週期並不相同。 • 而什麼是ASP.NET應用程式呢? • ASP.NET應用程式並非是單一個網頁,而是Web伺服器(如IIS+.NET Framework)上單一虛擬目錄(及其子目錄)範圍內的所有檔案、網頁、泛型處理處理常式(.ashx)、模組與程式碼的總和。 • 簡單來說,一個ASP.NET應用程式就是一個虛擬目錄(及其子目錄),當中可能包含各種資源檔案 • 例如範例7-3的ASP.NET應用程式代表ch07_03、ch07_03\App_Data內所有檔案(ch07_03.aspx、MyAdRotator.xml、web.config)的總和。 • 換句話說,一個ASP.NET應用程式可能包含許多網頁。
10.1 ASP.NET應用程式 • 【註】 • 虛擬目錄是IIS管控Web應用程式的方式,它以單一目錄為單位,將各個事先建立的虛擬目錄視為一個單位,我們在前面開發的範例是以檔案系統建立,也是以一個目錄為單位,當實際部署時,可以複製整個目錄到虛擬目錄內,若您安裝VWD的機器上已經安裝IIS,也可以將範例目錄直接設定為虛擬目錄,就不需要進行檔案的搬移。
10.1.1 ASP.NET應用程式概觀 • ASP.NET應用程式的原文是ASP.NET Application,它是一種Web Application。 • 而一般我們執行的視窗程式則稱為Windows Application。 • 要執行Windows Application,您可以點選Windows Application內的.exe執行檔啟動Windows Form Application的第一個視窗表單;而在WPF Application(另一種應用程式類型)中,則可以採用Application.Run(Windows)啟動WPF Application並開啟指定的視窗。 • 但要啟動ASP.NET Application則應該在瀏覽器網址列中,輸入該虛擬目錄內的某一個.aspx檔,按下網址列右方的後,瀏覽器發出對該.aspx的要求(Request)傳送至伺服器後就可以啟動ASP.NET Application。 • 由於Windows Form Application可以包含多個表單(Form),而表單資料需要互相存取時,只需要利用Form物件就可以達到,因為Form物件不一定關閉時就會被釋放,也可以採用隱藏模式。
10.1.1 ASP.NET應用程式概觀 • 對於ASP.NET Application而言,管理Application情況變得複雜許多,因為一個Application可能包含許多的Page,而有些資料可能需要在不同的Page間共用,比在Windows Form Application之間存取資料難上許多 • 因為Page物件(Page執行個體)一旦執行完輸出後,很快就會被釋放。 • 此外,啟動ASP.NET Application可能是由要求虛擬目錄內不同的網頁所引起 • (如要求ch09_04.aspx或Error1.aspx都可以啟動[ch09_04 ]ASP.NET Application) • 因此管理ASP.NET Application較為複雜。 • 回顧5.5.1節,我們曾介紹ASP.NET應用程式的生命週期,當時我們簡略了許多流程,在本節中,我們將重新介紹ASP.NET應用程式的生命週期。而在此之前,我們必須重新釐清ASP.NET是如何運行的。
10.1.2 ASP.NET的運行 • 要理解ASP.NET的運行,我們可由瀏覽器發出要求(Request)開始追蹤。假設伺服器端是裝載(hosting) ASP.NET的IIS 6伺服器 • (使用IIS5.1、IIS6、IIS7在對應策略上有些微不同) • (以下我們使用IIS/ASP.NET來表示這類伺服器)。 • 當瀏覽器發出Request經由網路送到上述的IIS 6/ASP.NET伺服器時,會進行下列步驟:
10.1.2 ASP.NET的運行 • 1伺服器端的HTTP聆聽程式(HTTP Listener)收到此一要求後,會將之轉交給URL指定的網站應用程式的工作流程處理器(aspnet_isapi.dll),稱之為ISPAPI。 • 2工作流程處理器解析URL,然後啟動System.Web.Hosting.ISAPIRuntime物件,接收HTTP要求(Request),並呼叫執行HttpRuntime.ProcessRequest()。 • 3而在ProcessRequest()中,會使用HttpApplicationFactory建立新的HttpApplication執行個體(或指定的 IHttpHandler處理器;事實上HttpApplication已經實作了IHttpHandler)。 • 而在此時,要求(Request)的內容會被包裝為HttpContext物件,成為HttpApplication的Context屬性,而其中的Request與Response屬性記錄了要求與回傳的相關資訊。
10.1.2 ASP.NET的運行 圖10-1 ASP.NET架構圖
10.1.2 ASP.NET的運行 • 在圖10-1中,應用程式定義域(Application Domain;由AppDomain類別管理)是第一次接收到執行應用程式時所建立 • 它是由ASP.NET的System.Web.HostingApplicationManager物件所建立,目的是要用來管理伺服器中的各個Application • 它會規劃一個區域給該Application使用,使得不同的Application間的變數不會互相影響。 • 在圖10-1中,每當一個新的瀏覽器(例如客戶端C)第一次發出執行該ASP.NET 應用程式的某個網頁之要求時,都會新增一個System.Web.HttpApplication執行個體(例如執行個體C) • 而HttpApplication執行個體一次只會處理一個要求 • 當回應該要求後,HttpApplication執行個體所佔用的記憶體就會被ASP.NET回收,如此伺服器的記憶體才不致因為來自四面八方的千萬個要求而消耗殆盡。
10.1.2 ASP.NET的運行 • 上述介紹的流程,包含了圖10-1的向右流程的前半部分。而當HttpApplication執行個體產生後,就會開始運作 • 它會先從驗證要求是否包含惡意攻擊的檢查開始進行一連串的動作,如圖5-4 • 當中包含了編譯與執行網頁 • 其中產生網頁執行個體是透過PageHandlerFactory建立Page執行個體來執行網頁的程式碼。
第五章之圖5-4 圖5-4 ASP.NET應用程式生命週期的HttpApplication管線「執行鏈」流程
10.1.2 ASP.NET的運行 • 假設客戶端要求執行應用程式的TEST.aspx,則執行網頁前的流程如圖10-2。 圖10-2 HTTP要求(Request)的處理流程
10.1.2 ASP.NET的運行 • 而當網頁執行完畢後會引發HttpApplication的PostRequestHandlerExecute事件,接著HttpApplication將要回傳的資料回傳給ISAPIRuntime與ISPAPI(aspnet_isapi.dll),最後透過HTTP聆聽程式(HTTP Listener) 回傳給客戶端,亦即圖10-1的向左流程。 • 而當HttpApplication執行個體執行完EndRequest、PreSendRequestHeaders、PreSendRequestContent等最後的三個事件對應的事件程序後就會被ASP.NET回收。 • 其中EndRequest是在回傳資料給ISAPIRuntime/ISPAPI時發生。
10.1.3 Global.asax與HttpApplication • 我們現在明白,每一個客戶端執行某一個ASP.NET應用程式時,是透過一個HttpApplication執行個體(實體)進行運作,換句話說,沒有透過HttpApplication執行個體(實體),則根本不會產生Page執行個體執行網頁程式。 • 那麼我們要如何應用這個HttpApplication執行個體呢?這裡有一個基本的問題,若我們不知道HttpApplication執行個體的參考名稱是什麼,要如何撰寫事件程序? • 例如Button1隸屬於Button類別,而執行個體的參考名稱就是Button1,所以我們可以撰寫Button1_Click事件程序對應Button1被按下時要執行的程式。
10.1.3 Global.asax與HttpApplication • 不過還記得嗎? • 我們在撰寫Page執行個體的事件程序時,使用的是Page作為假物件名稱,例如Page_Load • 在第五章曾經提及,使用單一檔案模式開發.aspx時,網頁類別衍生自基底 Page 類別(未設定Inherits屬性時),類別名稱實際上是「ASP<Page>_aspx」或「<Page>_aspx」,其中<Page>為檔案的主檔名 • 而ASP.NET為了讓一般使用者能夠方便使用,因此規定代表網頁執行個體的事件程序之命名慣例為「Page_事件名稱」(或可將此視為ASP.NET特殊的命名規範)。
10.1.3 Global.asax與HttpApplication • 而代表單一應用程式執行個體的事件程序之命名慣例則為「Application_事件名稱」 • 例如Application_BeginRequest、Application_Error等事件程序,代表當HttpApplication執行個體發生BeginRequest與Error事件時,會自動執行的事件程序。 • 另外一個問題則是,我們應該要在哪裡撰寫HttpApplication執行個體的事件程序呢? • 當然程式碼不會是撰寫於任一個.aspx網頁中。(否則若兩個aspx都定義了相同的HttpApplication事件程序,要執行哪一個?) • ASP.NET規定,HttpApplication應該撰寫於Global.asax(稱之為全域應用程式類別)檔中,並且此檔案應該放置於ASP.NET應用程式的根目錄(也就是虛擬目錄之根目錄中,而非子目錄中)。
10.1.3 Global.asax與HttpApplication • 而若您建立了Global.asax檔,則ASP.NET會將之編譯為衍生自HttpApplication類別的類別,編譯時會自動將應用程式事件程序繫結至Global.asax檔中的事件程序,然後使用衍生類別產生應用程式執行個體,故會執行相關的事件程序。 • HttpApplication規範的事件有很多,並且引發事件有一定的順序(除了Error事件之外),因此執行事件程序也有一定的順序。下表對照圖5-4,依照順序列出各種事件的順序:
10.1.3 Global.asax與HttpApplication • 【說明】 • 由於Error事件在任一階段都可以引發,因此可能會略過某些事件,所以只有EndRequest保證會發生,而它也是HTTP管線執行鏈裡送出回應前的最後一個事件。剩餘的PreSendRequestHeaders、PreSendRequestContent兩個事件都是當送出回應時才會發生。 表10-1 HttpApplication類別的事件及順序
10.1.3 Global.asax與HttpApplication • 獨立的HttpApplication執行個體 • 表10-1的事件可發生於每一個HttpApplication執行個體,並執行對應的事件程序,這些事件程序在每一個HttpApplication執行個體中都存在一個複本,故HttpApplication執行個體之間並不會互相影響 • 例如,在範例9-5 (可使用VWD開啟範例檔對照)中,若客戶端A要求執行ch09_05.aspx,則它會透過屬於自己的HttpApplication執行個體A執行ch09_05.aspx • 而若同時間客戶端B也要求執行ch09_05.aspx,則它也會透過屬於自己的HttpApplication執行個體B執行ch09_05.aspx。 • 換句話說,可能有兩個HttpApplication執行個體各自提供客戶端A與客戶端B的服務。
10.1.3 Global.asax與HttpApplication • 此處假設HttpApplication執行個體A,是收到客戶端A要求執行ch09_05.aspx時,ASP.NET配置給它的執行個體。 • 而假設HttpApplication執行個體B,是收到客戶端B要求執行ch09_05.aspx時,ASP.NET配置給它的執行個體。 • 當客戶端看到完整的網頁畫面時,HttpApplication執行個體A與B就因為完成服務將要結束其生命週期而被回收了。 • 當客戶端A輸入33,並按下Button2時,這是一個新的要求(PostBack要求),假設ASP.NET配置給它的為HttpApplication執行個體C。 • 而客戶端B也是輸入33,但按下Button1,此時也是一個新的要求(PostBack要求),假設ASP.NET配置給它的為HttpApplication執行個體D。
10.1.3 Global.asax與HttpApplication • 而重新觀察ch09_05.aspx的程式就會知道,HttpApplication執行個體C執行網頁時會發生例外 • 而由於例外並未被Try...Catch...EndTry捕捉,也未在網頁層級的Page_Error中處理,因此就變成了應用程式層級的錯誤,而引發了HttpApplication執行個體C的Error事件,並執行Application_Error事件程序。 • HttpApplication執行個體D執行網頁時也會發生例外,而此時雖然發生例外 • 但由於被Try...Catch...EndTry捕捉並處理,故不會形成網頁層級的錯誤,此時仍舊會按照原定的順序繼續執行,網頁執行完畢後,會引發PostRequestHandlerExecute事件,並沒有引發Error事件,故不會執行Application_Error事件程序。 • 如此,客戶端A與客戶端B雖然都執行相同的網頁(或相同應用程式內的不同網頁),但互相並不影響。
10.1.3 Global.asax與HttpApplication • Application_Start和Application_End • 在Global.asax檔案中,除了撰寫表10-1事件的事件程序外,另外還可以撰寫Application_Start與Application_End兩個事件程序。 • 而這兩個事件程序並不是一般的HttpApplication事件程序。ASP.NET只會在應用程式定義域的存留期各呼叫一次,而不是為每一個HttpApplication 執行個體都進行呼叫。
10.1.3 Global.asax與HttpApplication • Application_Start: • 在產生應用程式定義域時被呼叫,而產生應用程式定義域是當第一次收到HTTP要求執行應用程式的第一個資源 (例如網頁) 時就被產生。 • 此時應用程式定義域中只有第一個建立的HttpApplication執行個體,所以在此程序中並不適合設定動態資料。一般在此程序中,我們會寫入一些ApplicationState集合變數的初始值 • (ApplicationState集合將於10.4.2節介紹,它是一個可分享給所有HttpApplication執行個體、所有Page執行個體存取的集合)。
10.1.3 Global.asax與HttpApplication • Application_End: • 只有在卸載應用程式之前,才會被呼叫。而應用程式只有在伺服器被關閉或重新啟動時才會被卸載 • 一般我們會在此程序中,將需要保存的ApplicationState集合變數寫入檔案或資料庫以便重新啟動後,仍有機會可以取得這些資料。 • 由上述可知,應用程式的啟動與卸載,發生於該應用程式內的資源第一次被某個客戶端要求時,而卸載則發生於伺服器被關閉時 • 不過還有一種狀況也會重新啟動應用程式,也就是當您修改了應用程式的原始程式碼,而使得必須重新編譯時,也會重新啟動應用程式。
10.1.3 Global.asax與HttpApplication • 【註】 • 在ASP.NET中Application_OnStart與Application_Start、Application_OnEnd與Application_End是完全等義的。 • 不過經過測試,若同時存在時,編譯並不會出現問題 • 例如同時存在Application_OnStart與Application_Start可通過編譯,但只會執行Application_OnStart而不會執行Application_Start。
10.1.3 Global.asax與HttpApplication • Init()和 Dispose()方法 • 相對於Application_Start、Application_End是對整個應用程式作用,若希望讓每一個HttpApplication執行個體都自動在第一時間執行以及在最後時刻執行某些程式碼,可以透過覆寫Init和 Dispose方法來達成 • 它們分別會在HttpApplication執行個體產生與消滅時自動呼叫這兩個方法。 • 而覆寫的位置同樣是在Global.asax檔案中,如下語法:(我們會在範例10-14中,使用這個語法) Overrides Sub Init() Application("InstanceNum") = Application("InstanceNum") + 1 End Sub Overrides Sub Dispose() Application("InstanceNum") = Application("InstanceNum") - 1 End Sub
10.1.3 Global.asax與HttpApplication • 【範例10-1】 • 撰寫一個程式,利用HttpApplication的事件與方法,使得任何客戶端讀取應用程式內的任何一個網頁時,都會獲得空白的網頁(只顯示特定內容)。 • 範例10-1: • 網站目錄 ASPNET\ch10\ch10_01\ • Step1:在ch10_01目錄中,建立ch10_01a.aspx、ch10_01b.aspx兩個網頁,介面設計如下(各有一個Label1標籤控制項)。
10.1.3 Global.asax與HttpApplication • Step2:在方案總管中,點選網站,執行快顯功能表的【加入新項目】指令。
10.1.3 Global.asax與HttpApplication • Step3:點選「全域應用程式類別」,按下【加入】鈕。
10.1.3 Global.asax與HttpApplication • Step4:此時會出現Global.asax,並且在主視窗中會出現該檔案的預設內容,如下圖: 【註】使用VWD建立的Global.asax,預設有5個事件程序,其中Session_Start與Session_End將於後面章節說明。
10.1.3 Global.asax與HttpApplication • Step5:在Global.asax中,加入兩個HttpApplication的事件程序如下(加入完畢請存檔): • 程式部分內容(Global.asax):
10.1.3 Global.asax與HttpApplication • 範例說明: • (1)第5~10行:當BeginRequest事件發生時(由表10-1可知發生之時機為第一個事件)將執行此事件程序 • CompleteRequest()是HttpApplication的一個方法,功能是略過HTTP 管線執行鏈裡的所有事件和篩選,並且直接引發EndRequest事件而執行Application_EndRequest事件程序。 • 請注意,此程序之引數sender其實就是HttpApplication執行個體本身。因此,我們使用了HpAp與sender兩種方式來做示範。 • (2)第12~16行:任何一個要求(Request)都會執行Application_EndRequest(),而此程序之引數sender也是HttpApplication執行個體本身。
10.1.3 Global.asax與HttpApplication • (3)請注意查閱表10-1及圖5-5,當執行Application_EndRequest()時,代表網頁已經執行完畢並且準備將網頁執行結果回傳至客戶端,而由於我們是在Application_BeginRequest()內利用CompleteRequest()直接略過其他事件,前往執行Application_EndRequest(),故根本不會有Page執行個體的產生與執行。 • (4)不論您提出的要求是讀取ch10_01a.aspx還是ch10_01b.aspx,結果都一樣,都會被略過Page執行個體的產生與執行,而執行結果所出現的文字,其實是第8行與第15行的輸出。 • Step6:執行程式。
10.1.3 Global.asax與HttpApplication • 【範例10-2】 • 撰寫一個程式,不論讀取應用程式的任何一個網頁,在結尾處都加上一個版權宣告。 • 範例10-2: • 網站目錄 ASPNET\ch10\ch10_02\ • Step1:在ch10_02目錄中,建立ch10_02a.aspx、ch10_02b.aspx網頁,各有一個Label1標籤控制項。
10.1.3 Global.asax與HttpApplication • Step2:同範例10-1的Step2~4,建立Global.asax檔。 • Step3:在Global.asax中,加入Application_EndRequest事件程序如下(加入完畢請存檔): • 程式部分內容(Global.asax): • 範例說明: • (1)第8行:由於任何一個要求(Request)都會執行Application_EndRequest(),並且執行時已經完成了Page物件的執行,故在此使用Response.Write進行輸出,會加入在準備輸出的HTML內容的尾端。
10.1.3 Global.asax與HttpApplication • (2)在這裡我們使用了HttpContext,由圖10-1,我們可知,每一個要求都會被包裝在HttpContext進行處理,而它擁有Request與Response兩個屬性可分別讀取要求與輸出對該要求的回應。 • 不過,ASP.NET也允許透過HttpApplication執行個體直接使用這兩個屬性。因此,將第8行改寫為sender.Response.Write結果也是一樣的。 • 因為不論是sender.Response或sender.Context.Response都是參考到同一個HttpResponse物件。 • Step4:執行程式。 • 【註】想要在頁尾加上版權宣告,更好的方式是採用主版頁面設計方式,我們會在後面章節中介紹。
10.1.4 應用程式層級的錯誤處理 • 在前面我們已經提及,當發生應用程式層級的錯誤時,會引發Error事件,此時會執行Application_Error事件程序。而若沒有撰寫Application_Error事件程序或在Application_Error事件程序中未清除錯誤,則它會依照Web.config的設定進行網頁的導向。 • 請注意表10-1,在HttpApplication執行個體的管線執行鏈中,第二個動作是讀取Web.config檔,如果您在Web.config檔中設定了<customErrors>標籤,則ASP.NET會導向到defaultRedirect屬性設定的網頁網址。流程如下:
10.1.4 應用程式層級的錯誤處理 • 【範例10-3】 • 撰寫一個會發生例外的程式,不在事件程序與網頁層級處理,使之成為應用程式層級的錯誤,並在Application_Error中處理並清除它。 • 範例10-3: • 網站目錄 ASPNET\ch10\ch10_03\(檔案ch10_03.aspx) • Step0:ch10_03.aspx與ch09_03.aspx完全相同,但刪除了Page_Error與Page_PreRender事件程序。 • Step1:同範例10-1的Step2~4,建立Global.asax檔。 • Step2:在Global.asax中,修改Application_Error事件程序如下(修改完畢請存檔):
10.1.4 應用程式層級的錯誤處理 • 程式部分內容(Global.asax):
10.1.4 應用程式層級的錯誤處理 • 範例說明: • (1)當錯誤未在網頁層級的Page_Error事件程序中清除時,會引發HttpApplication執行個體的Error事件,成為應用程式層級的錯誤,並且執行Application_Error事件程序。 • (2)HttpApplication同樣具備Server屬性,型別也是HttpServerUtility,故可以使用表9-2的方法取出或清除錯誤。 • (3)在第18行取出的錯誤是已經被包裝過的錯誤,型別通常是HttpUnhandledException(代表未處理的例外),要取出原本的錯誤型別,可以利用GetBaseException方法來完成,如第21行。 • (4)第26行記得要清除例外,否則仍將出現發生例外的錯誤網頁。 • Step3:執行程式。
10.1.4 應用程式層級的錯誤處理 • 【範例10-4】 • 在Web.config檔案中,設定網頁發生錯誤時要移往的錯誤網頁。 • 範例10-4: • 網站目錄 ASPNET\ch10\ch10_04\(檔案ch10_04.aspx) • Step0:ch10_04.aspx與ch10_03.aspx完全相同。但網站中不包含Global.asax檔,並且事先預備一個要用來表示錯誤的網頁Error1.aspx(內容見Step4說明)。
10.1.4 應用程式層級的錯誤處理 • Step1:在VWD中,執行【網站/ASP.NET組態】指令,此時會開啟ASP.NET網站管理工具於瀏覽器,然後依照下圖操作。