基於功能更豐富的基礎類構建您自己的 ASP.NET 頁面
Dino Esposito
Wintellect
適用範圍:
Microsoft ASP.NET
Microsoft ASP.NET 2.0
摘要:通過繼承可以在通用 Microsoft ASP.NET 類(例如 Page 類)中添加功能。這爲您提供了一個公共場所,使您可以添加功能並將功能部署到所有頁面上。在本文中,Dino 將向您介紹如何添加頁面刷新處理、對冗長進程的支持以及使用 Page 類設置焦點控件。(本文包含一些指向英文站點的鏈接。請注意,在示例文件中,程序員的註釋使用的是英文,本文中將其譯爲中文是爲了便於讀者理解。)
本頁內容
構建功能更豐富的基礎類 | |
檢測瀏覽器刷新 | |
使用頁面刷新事件 | |
使用戶在冗長操作過程中獲得愉快體驗 | |
設置焦點控件 | |
結論 |
所有 Microsoft ASP.NET 頁面均來自由 System.Web.UI.Page 類表示的通用基礎頁面。爲了處理對 .aspx 資源的請求,ASP.NET 運行庫將創建一個動態類,並使該類繼承基礎 Page 類,或繼承其他反過來又繼承基礎 Page 類的類。如果在支持內含代碼模型的 Microsoft Visual Studio .NET 2003 項目中創建頁面,動態創建的 Page 類將繼承內含代碼類,而內含代碼類反過來又繼承基礎 Page 類。
基礎 Page 類實現典型的 ASP.NET 頁面生命週期(加載-回發-渲染週期),併爲衍生頁面提供一組預定義的成員和功能,例如,回發檢測、腳本插入、渲染和視圖狀態管理。
總而言之,System.Web.UI.Page 類只是一個基礎類,可用來定義一組通用的、最基本的功能和行爲。在特定的應用程序中,頁面很可能針對更多功能,並提供更強大的編程接口。有兩種可能的擴展方式:對頁面基礎結構進行一般意義上的增強,或者針對應用程序而改進功能。前一種擴展的示例是用來表示頁面菜單和菜單項的屬性。針對應用程序的頁面通常是根據由靜態區域、通用區域和可自定義區域組成的邏輯“母版”頁設計的。這些區域的內容可能因頁面而異,它們通常通過模板、佔位符和用戶控件進行填充。
請注意,在 Microsoft ASP.NET 2.0 中,母版頁的引入大大簡化了使用自定義和可編程屬性和成員針對應用程序而創建頁面的過程。
但是,如果您需要爲頁面創建功能更豐富、更復雜的基礎結構,應該怎麼辦?如何指示所有頁面提供其他系統級別的功能,例如,檢測 F5(刷新)鍵的功能?實現給定技巧所需的代碼始終可以與每個特定頁面的操作代碼(包含在內含代碼類中的代碼)合併在一起。但是,即使只實現了兩三個功能,生成的代碼的質量已經開始類似於大家唾棄的意大利麪條式代碼了,這是非常危險的。您必須尋找其他方法。
構建功能更豐富的基礎類
一種更好的方法是創建新的基礎 Page 類,用它來代替標準 System.Web.UI.Page 類。在本文中,我將介紹幾項通用功能以及它們的一般實現,並將這些功能裝入一個新的、功能更豐富的 Page 類中。我要介紹的功能包括:
• |
檢測 F5(刷新)鍵。 |
• |
啓動並控制一個需要立即向用戶發送反饋頁面的冗長操作。 |
• |
在加載頁面時設置輸入焦點。 |
如果您經常光顧專門介紹 ASP.NET 的新聞組和社區站點,而且閱讀了大量文章、書籍和新聞稿,那麼您可能已經知道如何在 ASP.NET 1.x 應用程序上下文中分別實現上述各項功能。這裏的問題是,如何通過一個組件和一個入口點同時提供所有這些功能。
通過定義自定義 Page 類,您只需付出最少的努力便可在新的 .aspx 頁面上提供所有其他功能和服務並獲得最大回報。使用 Visual Studio .NET 2003 創建的內含代碼頁面的聲明方式如下:
public class WebForm1 :System.Web.UI.Page { : }
要使 Web 窗體類繼承非默認的 Page 類,只需按照如下所示更改基礎類型。
public class WebForm1 :Msdn.Page { : }
如果頁面不是在 Visual Studio .NET 2003 中創建的或者使用了內嵌代碼,那麼可以通過 @Page 指令中的 Inherits 屬性爲該頁面設置基礎類型。
<% @Page Inherits="Msdn.Page" ... %>
您可以按照此處所述逐個頁面地更改 ASP.NET 頁面的基礎類型,也可以使用配置文件中的 <pages> 節點。
<pages pageBaseType="Msdn.Page" />
<pages> 節點的 pageBaseType 屬性指示要用作所有動態創建的 Page 類的基礎類型的類的名稱和程序集。默認情況下,該屬性在 machine.config 文件中被設置爲 System.Web.UI.Page。您可以在應用程序的 web.config 文件中覆蓋該設置。
下面讓我們看看如何在實際應用中實現上述各項功能,以及如何將這些功能封裝到一個無所不包的類中。
檢測瀏覽器刷新
在幾個月前發表在 aspnetPRO Magazine 上的文章中,我概括介紹了當用戶按下瀏覽器中的 F5 鍵刷新當前頁面時,對這一過程進行檢測所需的操作步驟。頁面刷新是瀏覽器對特定用戶操作(按 F5 鍵或單擊“刷新”工具欄按鈕)的響應。頁面刷新操作是瀏覽器內部的一種操作,因爲瀏覽器不會爲事件或回調發出任何外部通知。從技術上講,頁面刷新是通過“簡單”重複最新請求來實現的。換句話說,瀏覽器將緩存已處理的最新請求,並在用戶單擊頁面刷新鍵時重新發布已處理的請求。
正是因爲所有瀏覽器(據我所知)不會爲頁面刷新事件提供任何類型的通知,所以服務器端的代碼(例如,ASP.NET、典型 ASP 或 ISAPI DLL)根本無法區分刷新請求與一般的提交或回發請求。爲了幫助 ASP.NET 檢測和處理頁面刷新,您需要構建能夠使兩個完全相同的請求看起來不同的環境機制。
瀏覽器通過重新發送上次發送的 HTTP 有效負載來實現刷新,並使副本看起來與原始版本不同(這是一項額外服務,需要添加額外的參數並且 ASP.NET 頁面必須能夠緩存這些參數)。下圖提供了我要構建的子系統的詳細視圖。
圖 1:爲使刷新請求看起來與回發/提交請求不同而設置的機制
會話上下文中處理的每個請求獲得一個唯一且遞增的票證號碼。ASP.NET 頁面在生成響應之前生成票證,並將其存儲在一個自定義的隱藏字段中發送給瀏覽器。當用戶提交新請求(從而導致回發顯示的頁面)時,隱藏字段(如果有)將自動附着到服務器請求中。
在 Web 服務器上,新的 HTTP 模塊將截取 AcquireSessionState 事件,從隱藏字段中檢索當前票證,並將其與內部緩存的上次處理的票證 ID 進行比較。(上次處理的票證存儲在會話狀態中。)如果當前票證大於上次處理的 ID,或者如果這兩個值都爲零,則說明請求是一般的提交或回發。除此之外,刷新 HTTP 模塊不會執行其他操作,並原封不動地傳遞請求。
如果上次處理的票證大於或等於當前票證,則將請求標識爲頁面刷新。在這種情況下,HTTP 模塊將只在該請求的 HTTP 上下文的 Items 集合中創建一個新條目。在 ASP.NET 中,HttpContext 對象表示請求的上下文,並在請求的整個生命週期中始終存在。HttpContext 對象的 Items 屬性是一個集合,可由 HTTP 模塊、工廠處理程序和處理程序使用,用於將自定義信息轉發給實際的頁面對象。Items 集合中存儲的所有內容對處理當前請求的過程中涉及到的所有組件均可見。這些信息的生命週期與請求的生命週期相同,因此,一旦生成響應,所有數據都將被銷燬。通過使用 HttpContext.Current 靜態屬性,可以從該過程中涉及到的任何類訪問當前請求的 HTTP 上下文。
刷新 HTTP 模塊將在 Items 集合中創建名爲 IsPageRefreshed 的新條目。該條目的布爾值表明是通過一般的提交/回發請求頁面還是通過刷新請求頁面。下面的列表顯示了刷新 HTTP 模塊的實現。
using System; using System.Web; using System.Web.SessionState; namespace Msdn { public class RefreshModule :IHttpModule { // IHttpModule::Init public void Init(HttpApplication app) { // 註冊管道事件 app.AcquireRequestState += new EventHandler(OnAcquireRequestState); } // IHttpModule::Dispose public void Dispose() {} // 確定是否正在處理 F5 或後退/前進操作 private void OnAcquireRequestState(object sender, EventArgs e) { // 訪問 HTTP 上下文 HttpApplication app = (HttpApplication) sender; HttpContext ctx = app.Context; // 檢查 F5 操作 RefreshAction.Check(ctx); return; } } }
RefreshAction 類包含用來確定當前請求是否是頁面刷新的邏輯。如果確定爲頁面刷新,HttpContext 的 Items 集合中將包含一個新條目:IsPageRefreshed 設置爲 true。
public static void Check(HttpContext ctx) { // 初始化票證字段 EnsureRefreshTicket(ctx); // 讀取會話中上次處理的票證(從會話中) int lastTicket = GetLastRefreshTicket(ctx); // 讀取當前請求的票證(從隱藏字段中) int thisTicket = GetCurrentRefreshTicket(ctx); // 比較兩個票證 if (thisTicket > lastTicket || (thisTicket==lastTicket && thisTicket==0)) { UpdateLastRefreshTicket(ctx, thisTicket); ctx.Items[PageRefreshEntry] = false; } else ctx.Items[PageRefreshEntry] = true; }
隱藏字段和會話字段的名稱在 RefreshAction 類中被設置爲公共常量,並且可以在該類的外部使用。
應用程序頁面如何利用此機制?什麼時候檢測頁面刷新真正有用?HTTP 模塊並不阻止任何請求,它只爲最終 ASP.NET 頁面添加更多信息以便處理請求。添加的信息包括表示頁面刷新的布爾值。
使用頁面刷新事件
Web 頁的用戶通常只執行幾個操作,而且從某種程度上講,執行這些操作時心情都很愉快。這些操作包括“後退”、“前進”、“停止”和“刷新”。但這些操作構成了一種 Internet 瀏覽器的標準工具包。截取以及細分這些操作可能會對普遍認可的 Internet 操作帶來某種“侷限性”。對用戶可能產生負面影響。
另一方面,當用戶刷新當前頁面或退回到先前訪問的頁面時,會向服務器提交已處理過的請求,這有可能會打斷應用程序狀態的一致性。在這種情況下,也可能對應用程序產生負面影響。
請設想以下情況:
您通過 DataGrid 顯示數據,並在每一行中提供一個按鈕,供用戶刪除所表示的數據行。儘管這是很常見的做法(輕輕點擊,即可刪除當前應用程序中實現的數據),但這種做法極其危險。用戶很容易由於失誤而單擊了錯誤的按鈕,從而破壞數據的一致性,而且如果他們在刪除(不管是有意還是無意)之後刷新頁面,則很可能會刪除第二個行。
當您刷新頁面時,瀏覽器只重複上次發佈的內容。從 ASP.NET 運行庫的角度來看,只有一個新請求要處理。ASP.NET 運行庫無法區分一般的請求和意外重複的請求。如果採取脫機工作的方式,並按內存中存儲的 DataSet 中的位置刪除記錄,則很可能會多刪除一條記錄。如果上一個操作以 INSERT 結束,刷新頁面更有可能會添加一條記錄。
這些示例清楚地暴露出某些有爭議的設計問題,但它們反映了完全可能的情況。那麼,阻止頁面刷新最好的方式是什麼呢?
本文前面討論的機制可以預處理請求,並確定是否正在刷新頁面。這些信息通過 HttpContext 對象傳遞給頁面處理程序。在頁面中,開發人員可以使用以下代碼檢索這些數據。
bool isRefresh = (bool) HttpContext.Current.Items["IsPageRefreshed"];
但更好的做法是,如果使用自定義的、更有針對性的 Page 類,則可以將數據封裝到一個更易於使用的屬性中,即封裝到 IsPageRefresh 屬性中。
public bool IsPageRefresh { get { object o = HttpContext.Current.Items[RefreshAction.PageRefreshEntry]; if (o == null) return false; return (bool) o; } }
通過使 Page 類繼承新的、功能更豐富的基礎類(本例中爲 Msdn.Page),可以通過新屬性瞭解發出請求的真正原因。以下示例顯示瞭如何實現不應在頁面刷新時重複的某個關鍵操作。
void AddContactButton_Click(object sender, EventArgs e) { if (!IsPageRefresh) AddContact(FName.Text, LName.Text); BindData(); TrackRefreshState(); }
僅當在不刷新頁面時才添加新聯繫人,換句話說,僅當用戶按照常規方式單擊“Add-Contact”(添加聯繫人)按鈕時纔會添加聯繫人。上述代碼片斷中有一個很奇怪的 TrackRefreshState 方法,它的作用是什麼呢?
該方法更新票證計數器,並確保新頁面響應包含帶有最新票證的隱藏字段。在本例中,通過將會話狀態中存儲的值遞增一來獲取下一個票證。(這裏只是隨便使用了會話狀態,最好不要使用會話狀態,而使用更具擴展性的提供程序模型,就像在 ASP.NET 2.0 中一樣。)
但是,關於 TrackRefreshState 方法(這是有意命名的,以便於大家回想起更熟悉的 TrackViewState 方法),主要有一點要說明。通過調用該方法,除了可以添加其他信息外,還可以將帶有當前請求票證的隱藏字段添加到頁面響應中。如果沒有隱藏字段(參見圖 1),刷新機制將無法檢測下一個回發是刷新還是提交。換句話說,通過在回發事件處理程序中調用 TrackRefreshState,使得系統知道您要跟蹤該操作(而且只跟蹤該操作),以確定是否爲頁面刷新。這樣,您只跟蹤可能會出錯的頁面刷新,而且並不是所有頁面刷新都會在會話生命週期內發生。
要利用頁面刷新功能,只需在 Microsoft Visual Studio .NET 項目中添加一個新頁面,然後打開內含代碼文件並將頁面的基礎類更改爲 Msdn.Page。接下來,在您執行不應刷新的操作時調用 TrackRefreshState(Msdn.Page 類的新的公共方法)。使用新的布爾屬性 IsPageRefresh 檢查刷新狀態。
使用戶在冗長操作過程中獲得愉快體驗
有關如何在 Web 上跟蹤特別耗時的操作這個問題,已經通過多篇文章和多次演講爲大家提供了各種解決方案。我所說的“耗時的操作”是指 Windows 窗體方案中通常需要進度欄的所有操作。在 Web 頁上顯示進度欄很容易出現問題。進度欄應該能夠與服務器通信,以獲取有助於更新進度的信息。此外,此操作不應通過回發或刷新元標記來完成,以免完全刷新頁面。在任何情況下,均需要具備強大的動態 HTML 支持。
要使用戶在冗長操作過程中獲得愉快體驗,相對簡單的方法就是顯示一箇中間反饋頁面,爲用戶顯示一些等待消息,最好是帶點動畫。此頁面完全與上下文無關,但無疑要比加載新頁面之前在空白頁面上長時間顯示一個沙漏更有用。
要在冗長操作過程中顯示一些反饋,有一種簡單而有效的方法,它可以概括成以下幾個步驟:
• |
一旦用戶通過單擊開始該任務,便將用戶重定向到反饋頁面。反饋頁面必須知道實際執行任務的頁面的 URL。此 URL(包括會話狀態)可以通過查詢字符串進行傳遞,也可以放置在可訪問的數據存儲中。 |
• |
開始加載反饋頁面後,再重定向到工作頁面。這種情況下,重定向是由頁面的 onload Javascript 事件中的腳本完成的。瀏覽器加載並顯示反饋頁面,然後指向工作頁面。頁面開始執行冗長的任務,同時爲用戶顯示反饋頁面。 |
• |
根據需要,反饋頁面可以很複雜幷包括許多 UI 元素。它可以包含“請稍候...”消息或顯示動畫 GIF,或者藉助某些動態 HTML 功能,顯示某些看起來像是一個真正進度欄的內容。 |
我特意創建了一個 LengthyAction 類來幫助管理冗長任務的開始。
private const string UrlFormatString = "{0}?target={1}"; public static void Start(string feedbackPageUrl, string targetPageUrl) { // 準備反饋頁面的 URL string url = String.Format(UrlFormatString, feedbackPageUrl, targetPageUrl); // 將調用重定向到反饋頁面 HttpContext.Current.Response.Redirect(url); }
該類的特點是隻有一個靜態方法,即 Start。Start 方法獲取反饋頁面和目標頁面(即執行任務的頁面)的 URL。該方法將兩個參數合併成一個 URL 並進行重定向。
反饋頁面可以包含您希望的任何用戶界面,但必須滿足幾個關鍵的要求。該頁面必須能夠檢索工作頁面的名稱,並提供一個可能的自動機制,以便通過腳本重定向到工作頁面。我定義了一個自定義的基礎 Page 類,並將這些功能內置在該類中。這樣做時,我必須進行一些假定。特別是,我的實現假定工作頁面的名稱使用大家熟知的屬性名稱 target 通過查詢字符串進行傳遞。目標頁面的名稱存儲在名爲 TargetURL 的公共屬性中。此外,反饋頁面提供名爲 GetAutoRedirectScript 的函數。此函數的目的是返回通過腳本實現重定向所需的腳本代碼。
public string GetAutoRedirectScript() { return String.Format("location.href='{0}';", TargetUrl); }
爲了使問題儘可能地簡單,FeedbackBasePage 類還查找名爲 Body 的通用 HTML 控件。這與您從以下標記中獲取的完全一樣。
<body runat="server" id="Body">
如果可以通過簡單的方法爲頁面的正文標記編程,FeedbackBasePage 類將找到這種方法並自動添加 onload 屬性;否則,您必須手動添加 onload 屬性。要使反饋頁面正常工作,此屬性是必需的。
HtmlGenericControl body = FindControl(BodyId) as HtmlGenericControl; if (body != null) body.Attributes["onload"] = GetAutoRedirectScript();
最後提供給瀏覽器的標記代碼如下所示。
<body οnlοad="location.href='lengthyop.aspx'">
讓我們看看使用本文中討論的類實現冗長操作需要執行哪些步驟。
首先引用所需的程序集,然後爲觸發操作的單擊按鈕編寫以下事件處理程序。
void ButtonLengthyOp_Click(object sender, EventArgs e) { LengthyAction.Start("feedback.aspx", "work.aspx"); }
接下來,在項目中添加反饋頁面。這是一個常規 Web 窗體頁,您可以按照上文所述修改其 <body> 標記,並將基礎類更改爲 FeedbackBasePage。在您單擊按鈕開始進程之後以及顯示結果之前,將顯示反饋頁面的用戶界面,如下圖所示。
圖 2:冗長操作的順序
在本例中,我使用了一種跨頁回發,這對特別冗長的操作來說是一種更普遍的方案。但是,這引發了傳遞視圖狀態以及工作頁面完成其任務通常所需的參數的問題。您可以使用工作頁面的查詢字符串將序列化的對象版本連接起來,或者將所有內容都存儲在 ASP.NET 緩存或 Session 對象中。這種情況下不能使用 HTTP 上下文,因爲該操作涉及多個 HTTP 請求,每個請求都有一個不同的項目集。
請注意,反饋頁面的 URL 包含調用的某些細節,並且可能如下所示。
feedback.aspx?target=work.aspx?param1=123¶m2=hello
要隱藏這些細節,您可以定義一個自定義 HTTP 處理程序,並將其綁定到您認爲更合適的虛擬 URL。該 HTTP 處理程序可以從緩存或會話狀態中檢索所需的信息(包括反饋頁面和工作頁面的名稱)。
設置焦點控件
ASP.NET 2.0 提供了一個非常好的新功能,允許您指定首次顯示頁面時將哪個輸入控件設置爲焦點。這是一種靈活的功能,可以減少用戶通過單擊開始操作的負擔,例如,在文本框中單擊開始輸入數據。
要將 HTML 組件指定爲輸入焦點,您需要一小段 Javascript 代碼。首先聲明一點:這不是尖端的火箭科學,您可以輕鬆地將這段 Javascript 代碼作爲內嵌代碼添加到 <body> 標記的 onload 屬性中。但是,在 Page 類上使用 SetFocus 方法確定服務器上的焦點控件的名稱確實是前進了一大步。實際上,您可以在 ASP.NET 2.0 中使用以下代碼。
void Page_Load(object sender, System.EventArgs e) { SetFocus("TheFirstName"); }
當顯示頁面時,名爲 TheFirstName 的輸入控件將成爲焦點。此方法便捷有效,但如何在 ASP.NET 1.x 中對其進行編碼?
同樣,實現此功能的技巧已爲業界人士所熟知,也可以從 Google 中毫不費力地搜索到。但問題是,如何將其集成到基礎 Page 類中以便重複使用。
讓我們使用以下聲明來擴展 Msdn.Page 基礎類。
private string m_focusedControl; public void SetFocus(string ctlId) { m_focusedControl = ctlId; }
SetFocus 方法收集控件的 ID 並將其存儲在內部成員中。在頁面的 PreRender 事件中,調用另一個幫助程序函數以構建和插入 Javascript 代碼。
private void AddSetFocusScript() { if (m_focusedControl == "") return; // 添加腳本以聲明函數 StringBuilder sb = new StringBuilder(""); sb.Append("<script language=javascript>"); sb.Append("function "); sb.Append(SetFocusFunctionName); sb.Append("(ctl) {"); sb.Append(" if (document.forms[0][ctl] != null)"); sb.Append(" {document.forms[0][ctl].focus();}"); sb.Append("}"); // 添加腳本以調用函數 sb.Append(SetFocusFunctionName); sb.Append("('"); sb.Append(m_focusedControl); sb.Append("');<"); sb.Append("/"); // 按照這種方式斷開,以避免誤解... sb.Append("script>"); // 註冊腳本(名稱區分大小寫) if (!IsStartupScriptRegistered(SetFocusScriptName)) RegisterStartupScript(SetFocusScriptName, sb.ToString()); }
Javascript 代碼可以像動態字符串一樣構建,並累積存儲在 StringBuilder 對象中。下一步是將該字符串添加到頁面輸出中。在 ASP.NET 中,要在頁面中添加一些客戶端腳本代碼,必須先在特定頁面級別的集合中註冊該代碼。爲此,Page 類提供了幾個 RegisterXxx 方法。每個 RegisterXxx 方法將 Javascript 代碼塊添加到不同的集合中,以便插入到最終頁面標記中的不同位置。例如,RegisterStartupScript 在窗體的關閉標記之前插入代碼。而 RegisterClientScriptBlock 在窗體的打開標記之後插入腳本代碼。重要的是,必須在腳本中包括 <script> 元素的兩個標記。每個腳本塊都由一個關鍵字標識,這樣多個服務器控件可以使用同一個腳本塊,而不會將它發送給輸出流兩次或多次。
在頁面中,以下 Javascript 代碼塊被插入到窗體的關閉標記之前。這樣,它將在初始化後啓動時立即開始運行。
<form> : <script language=javascript> function __setFocus(ctl) { if (document.forms[0][ctl] != null) { document.forms[0][ctl].focus(); } } __setFocus('TheFirstName'); </script> </form>
通過在 Msdn.Page 類上使用 SetFocus 公共方法,您可以在頁面代碼的任何位置決定在瀏覽器中顯示頁面時將哪個控件作爲輸入焦點。更重要的是,您可以根據運行時條件和/或回發事件作出此決定。
結論
在面嚮對象的技術(例如 ASP.NET)中,主要優點之一是您可以廣泛使用繼承。通過繼承和改進現有控件的公共接口,您可以很輕鬆地創建新的自定義服務器控件。在衍生類中,您可以替代虛擬方法,從而改變組件的內部行爲。將這些面向對象的編程 (OOP) 原則應用於控件似乎非常自然,非常普遍,但對於表示 ASP.NET 頁的類,情況則不盡然。
不過,頁面繼承廣泛用於構建所請求的每個 .ASPX 頁面的可執行表示。內含代碼頁面是指從基礎 System.Web.UI.Page 類繼承的頁面。爲什麼不定義一箇中間 Page 類,爲應用程序特定的頁面提供功能更豐富的基礎呢?
這正是本文所闡述的問題。我在本文中介紹了許多開發人員或多或少都能成功實現的三個常見功能,即檢測刷新鍵、控制冗長操作以及將控件指定爲輸入焦點,還介紹瞭如何將這三個功能全部封裝到一個無所不包的 Page 類的上下文中。
內含代碼和內嵌代碼應用程序中使用了新的 Page 類(Msdn.Page 類)來取代基礎 Page 類,這個新類以方便且可重複使用的方式爲開發人員提供了更多基本功能。功能更豐富的基礎 Page 類是爲 ASP.NET 應用程序構建更可靠平臺的一個里程碑。實際應用程序的所有頁面都應從自定義類開始構建,以此來驗證 Page 類的功能。
參考資料