.net防止页面刷新的扩展的Page类

检测浏览器刷新 在几个月前发表在 aspnetPRO Magazine 上的文章中,我概括介绍了当用户按下浏览器中的 F5 键刷新当前页面时,对这一过程进行检测所需的操作步骤。页面刷新是浏览器对特定用户操作(按 F5 键或单击“

检测浏览器刷新

在几个月前发表在 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 检查刷新状态。

posted on 2011-03-01 10:15  人渐人爱  阅读(495)  评论(0)    收藏  举报

导航