来分析这样一种实际情况,即,在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模块中。
如下代码展示了该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仅适用于这里介绍的自定义页面类。自定义页面类并非只是添加该属性,它还要添加隐藏字段,这是该机制起作用所必不可少的。
转自:http://space.itpub.net/100788/viewspace-271432