asp.net 檢測頁面是否刷新



來分析這樣一種實際情況,即,在HTTP處理程序處理請求之前對請求進行篩選,這有助於實現一個原本不可能的特徵。回發機制有一個嚴重的缺陷——如果用戶刷新當前顯示頁面,則服務器上所採取的最後一個動作將盲目地重複。例如,如果作爲前一次發送的結果添加了一個新記錄,則應用程序會在另一次回發時試圖插入一個完全相同的記錄。當然,這會導致插入完全相同的記錄,因而應當產生一個異常。這一缺陷自Web編程最先出現時就已經存在了,ASP.NET無疑不會引入它。要實現非重複的動作,必須採取一些對策,本質上將任何關鍵的服務器端操作轉換爲一個冪等性。在代數中,如果一個操作不管對它執行多少次結果都不變,我們就說該操作是冪等的。例如,看一看如下SQL命令:

DELETE FROM employees WHERE employeeid=9

我們可以對該命令連續執行1000次,但是最多隻會刪掉1個記錄,即滿足WHERE子句中設定的標準的記錄。另請考慮如下命令:

INSERT INTO employees VALUES (...)

每次執行該命令,都有可能把一個新記錄添加到employees表中。如果存在自動編碼的鍵列或者非惟一的列,尤其會出現這種情況。如果表設計要求鍵是惟一的並且明確加以規定,則第2次運行該命令時會拋出一個SQL異常。

雖然剛纔考慮的特殊情況通常在數據訪問層(data access layer,簡稱DAL)解決,但是它的基本模式代表了大多數Web應用程序的一個常見方案。因此,待研究的問題是:怎樣查明頁面是因爲一個顯式的用戶操作被回傳,還是因爲用戶按下了F5鍵或頁面刷新工具欄按鈕呢?

1. 頁面刷新操作的基本原理

頁面刷新操作是一種內部瀏覽器操作,對此瀏覽器不會根據事件或回調提供任何外部通知。從技術上講,頁面刷新是由最新請求的“簡單的”重複組成的。瀏覽器緩存它所服務的最新請求,並在用戶按下頁面刷新鍵或按鈕時重新顯示。我所知道的瀏覽器不會爲頁面刷新事件提供任何類型通知——即使有,無疑也不是一種公認標準。

據此可知,服務器端代碼(例如,ASP.NET、經典ASP或ISAPI DLL)無法將刷新請求與一般的提交或回發請求相區分。爲了幫助ASP.NET檢測和處理頁面刷新,我們需要創建外圍機制,使兩個在其他方面相同的請求看起來不同。所有已知的瀏覽器都是通過重新發送最後發送的HTTP請求來實現刷新;爲了使該副本不同於原始請求,一個額外的服務必須添加其他參數,而 ASP.NET頁面必須能夠捕獲它們。

我考慮了一些附加需求。解決方案不應依賴會話狀態,而且不應使服務器內存負荷太重。它應該是相對容易部署的,而且應儘量不引人注目。

2. 解決方案的概要描述

本解決方案基於如下思想:每個請求被分配一個標籤號,而HTTP模塊將跟蹤它處理的每個不同頁面裏最後服務的標籤。如果該頁面持有的標籤號小於該頁面的最後服務的標籤,則只能表明服務了相同的請求——即,頁面刷新。該解決方案由兩個構造塊組成:一個HTTP模塊和一個自定義的頁面類,前者對標籤號作初步檢查,後者自動地將一個漸進的標籤號碼添加到每個服務過的頁面。使該特徵起作用涉及兩個步驟:首先,註冊該HTTP模塊;其次,在相關的應用程序中改變每個頁面的基本的代碼隱藏類以檢測瀏覽器刷新。

HTTP模塊位於HTTP運行庫環境的中間,登記應用程序中的一個資源的每個請求。頁面第一次被請求時(不是回發時),不分配任何標籤。HTTP模塊將生成一個新的標籤號,並把它存儲在HttpContext對象的Items集合中。此外,該模塊將最後服務的標籤的內部計數器初始化爲0。隨後該頁面每次被請求時,該模塊都將最後服務的標籤與頁面標籤進行比較。如果頁面標籤更新一些,則該請求被認爲是一次普通的回發;否則,它將被標記爲一次頁面刷新。表2.6總結了這兩種場景及其相關的操作。




爲了確保每個請求(除了第一次以外)都有一個合適的標籤號,需要得到頁面類的一些幫助。這就是爲什麼需要將每個打算支持該特徵的頁面的代碼隱藏類設置爲一個特定類——這是我們稍候將討論的一個過程。該頁面類將從HTTP模塊接收兩種不同的信息:要存儲在隨頁面一起傳送的一個隱藏字段中的下一個標籤,以及該請求是否爲頁面刷新的信息。作爲對開發人員的一項增值服務,代碼隱藏類將提供一個額外的布爾屬性:IsRefreshed,以允許開發人員瞭解請求是頁面刷新還是常規回發。

*重要提示    HttpContext類上的Items集合是一個載體集合,是爲了讓HTTP模塊將信息向下傳遞給實際負責服務請求的頁面和HTTP處理程序而特意建立的。我們這裏採用的HTTP模塊在Items集合中設置兩個數據項。一個數據項讓頁面知道請求是否爲頁面刷新;另一個數據項讓頁面知道下一個標籤號是什麼。讓HTTP模塊將下一個標籤號傳遞給頁面,滿足使頁面類的行爲儘可能地簡單和線性的目的,從而將大部分實現和執行負擔轉移給HTTP模塊。

3. 解決方案的實現

我剛剛概述的解決方案有幾個問題有待研究。首先,狀態是必需的,我們把它保存在哪裏?其次,對每個輸入請求都將調用一個HTTP模塊。如何區分對相同頁面的請求呢?如何把信息傳遞給頁面呢?你希望頁面有多大的智能呢?

顯然,這裏所列的每個問題,都可以用不同於此處所介紹的方法進行設計和實現。爲了得到一個可行的解決方案,這裏作出的所有設計選擇應當被認爲是任意的,如果需要對該代碼進行重新加工以更好地滿足自己的目的,可以用等效的策略替換它。下一個實例中給出的代碼版本,融入了我一直以來所收集的最寶貴的建議。這些建議之一如前一個重要提示所述,儘量將代碼移到HTTP模塊中。

public class RefreshModule : IHttpModule

{

public void Init(HttpApplication app) {

app.BeginRequest += new EventHandler(OnAcquireRequestState);

}

public void Dispose() {

}

void OnAcquireRequestState(object sender, EventArgs e) {

HttpApplication app = (HttpApplication) sender;

HttpContext ctx = app.Context;

RefreshAction.Check(ctx);

return;

}

}

該模塊監聽BeginRequest事件,結束調用RefreshAction輔助類上的Check方法。 

public class RefreshAction

{

static Hashtable requestHistory = null;
// Other string constants defined here
public static void Check(HttpContext ctx) {

// Initialize the ticket slot
EnsureRefreshTicket(ctx);

// Read the last ticket served in the session (from Session)
int lastTicket = GetLastRefreshTicket(ctx);

// Read the ticket of the current request (from a hidden field)
int thisTicket = GetCurrentRefreshTicket(ctx, lastTicket);

// Compare tickets
if (thisTicket > lastTicket ||(thisTicket==lastTicket && thisTicket==0)) {

UpdateLastRefreshTicket(ctx, thisTicket);

ctx.Items[PageRefreshEntry] = false;

}

else

ctx.Items[PageRefreshEntry] = true;

}

// Initialize the internal data store

static void EnsureRefreshTicket(HttpContext ctx)

{

if (requestHistory == null)

requestHistory = new Hashtable();

}

// Return the last-served ticket for the URL

static int GetLastRefreshTicket(HttpContext ctx)

{

// Extract and return the last ticket

if (!requestHistory.ContainsKey(ctx.Request.Path))

return 0;

else

return (int) requestHistory[ctx.Request.Path];

}

// Return the ticket associated with the page

static int GetCurrentRefreshTicket(HttpContext ctx, int lastTicket)

{

int ticket;

object o = ctx.Request[CurrentRefreshTicketEntry];

if (o == null)

ticket = lastTicket;

else

ticket = Convert.ToInt32(o);

ctx.Items[RefreshAction.NextPageTicketEntry] = ticket + 1;

return ticket;

}

// Store the last-served ticket for the URL

static void UpdateLastRefreshTicket(HttpContext ctx, int ticket)

{

requestHistory[ctx.Request.Path] = ticket;

}

}
Check方法操作如下:它將最後服務的標籤(如果有)與頁面提供的標籤進行比較。該頁面將標籤號存儲在一個通過Request對象接口讀入的隱藏字段中。HTTP模塊維護一個散列表,服務的每個不同的URL都有一個表項。該散列表中的值存儲該URL的最後服務的標籤。

注意    Item索引器屬性,來設置最後服務的標籤,因爲Item重寫已有的項。如果數據項已經存在,則Add方法只是返回。

除了創建HTTP模塊,我們還需要安排一個頁面類,以用作需要檢測瀏覽器刷新的頁面的基類。下面給出了這個頁面類的代碼:
// Assume to be in a custom namespace
public class Page : System.Web.UI.Page

{

public bool IsRefreshed {

get {

HttpContext ctx = HttpContext.Current;

object o = ctx.Items[RefreshAction.PageRefreshEntry];

if (o == null)

return false;

return (bool) o;

}

}

// Handle the PreRenderComplete event

protected override void OnPreRenderComplete(EventArgs e) {

base.OnPreRenderComplete(e);

SaveRefreshState();

}

// Create the hidden field to store the current request ticket

private void SaveRefreshState() {

HttpContext ctx = HttpContext.Current;

int ticket = (int) ctx.Items[RefreshAction.NextPageTicketEntry];

ClientScript.RegisterHiddenField(

RefreshAction.CurrentRefreshTicketEntry,

ticket.ToString());

}

}

該示例頁面定義了一個新的公共布爾屬性IsRefreshed。我們可以在代碼中以使用IsPostBack或IsCallback那樣的方法使用該屬性。該實例頁面重寫了OnPreRenderComplete方法,用頁面標籤添加隱藏字段。如前所述,該頁面標籤是通過Items集合中的一個特別的(並且是任意命名的)項從HTTP模塊中得到的。

 
public partial class TestRefresh : ProAspNet20.CS.Components.Page

{

protected void AddContactButton_Click(object sender, EventArgs e)

{

Msg.InnerText = "Added";

if (!this.IsRefreshed)

AddRecord(FName.Text, LName.Text);

else

Msg.InnerText = "Page refreshed";

BindData();

}

}

IsRefreshed屬性允許我們決定在一個回發動作被請求時要做什麼。在上述代碼中,如果頁面正在刷新,則不調用AddRecord方法。不用說,IsRefreshed僅適用於這裏介紹的自定義頁面類。自定義頁面類並非只是添加該屬性,它還要添加隱藏字段,這是該機制起作用所必不可少的。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章