相信大家也遇到一个问题(这个问题我经常遇到)那就是用户刷新当前显示页面,则服务器上采取的最后一个动作将盲目的重复。例如,如果前一次发送的结果是增加一条数据,则应用程序会在另一次回发时试图插入一个完全相同的数据。如图:
这时我先在数据库添加一条记录(xiaofeng,11),然后刷新两遍浏览器得到的结果。
页面刷新的基本原理
页面刷新是一种内部浏览器操作,对此浏览器不会根据任何事件或回调提供任何外部通知。浏览器会缓存它所有服务请求,并在用户按下页面刷新键或按钮时重新显示。浏览器也不会为刷新事件提供任何类型通知。那么由此可以知道服务器端代码无法将刷新请求与一般的提交或回发请求相区分。那么此时我们需要创建外围机制,使两个在其他方面相同的请求看起来相同。所有浏览器都是通过重新发送最后发送的HTTP请求来实现刷新。为了使该副本不同于原始请求,一个额外的服务必须添加到其他参数,而ASP.NET页面必须能捕获他们。
这里解决方案设计到HttpModule这块知识,所以先简单介绍下
Asp.net为每一个正在运行的应用程序管理一个 HttpApplication对象池,并从池中选出一个实例来服务特定的请求。这些对象基于 global.asax文件中定义的类或者基于HttpApplication基类。对请求负责的HttpApplication对象的最终目标是获得一个HTTP处理程序。在最终的HTTP处理程序的过程中,HttpApplication对象使请求通过一个由HTTP模块组成的通道。 HTTP模块是一个实现了IHttpModule接口的.NET Framework类。那些筛选请求中的原始数据的HTTP模块,在web.config文件中配置的多个系统 HTTP模块。HTTP模块可以对一个请求进行预处理和后处理,它截取处理系统事件以及其他模块产生的事件。
IHttpModule接口
它包含了两个方法:Init和Dispose。Inti方法初始化一个模块,并为它做好处理请求的准备。可以这里我们同意接受感兴趣的事件通知,它接受一个服务请求的HttpApplication对象的引用。使用该引用可以连接到系统事件。Application对象也有一个成为context的对象,该对象对ASP.NET应用程序内在属性的引用,这样就可以访问response,request,session等。Dispose方法处置该模块使用资源(除内存外的一切)。它典型的用法是关闭数据库连接或文件句柄。
我们编写自定义Http模块,就是实现IHttpModule接口,并在web.config配置文件中
下面接着上面谈谈解决方案
每个请求被分配一个标签号,而HTTP模块将跟踪它处理的每个不同页面里最后服务的标签。如果该页面特有的标签号小于该页面的最后服务的标签号 ,则只能表明服务了相同的请求即页面刷新。那么我们可以用一个HTTP模块对标签号作初步检查,一个自定义页面类自动地将一个渐进的标签号码添加到每个服务过的页面。
HTTP模块位于HTTP运行库环境的中间,登记应用程序中的一个资源的每个请求。页面第一次被请求时(不是回发),不分配任何标签。HTTP模块将生成一个新的标签号,并把它存储在HttpContext对象的Item集合中。此外,该模块将最后服务的标签的内部技术器初始化为0.随后该页面每次被请求时,该模块都将最后服务的标签与页面标签进行比较。如果页面标签大一些就认为使一次普通的回发;否则它将被标记为一次页面刷新。
为了确保每个请求都有一个合适的标签号,需要得到页面类的帮助。该页面类从HTTP模块接收两种不同的信息。要存储在随页面一起传送的一个隐藏字段中的下一个标签,以及该请求是否为页面刷新的信息。(HttpContext类上的Items集合是一个载体集合,是为了让HTTP模块将信息向下传递给实际负责服务请求的页面和HTTP处理程序而特意建立的。
具体实现
首先我们构建一个HttpModule模块
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
namespace Components
{
public class RefreshModule:IHttpModule
{
#region IHttpModule 成员
public void Dispose()
{
throw new NotImplementedException();
}
public void Init(HttpApplication app)
{
// 注册事件
app.BeginRequest += new EventHandler(OnAcquireRequestState);
}
#endregion
public void OnAcquireRequestState( object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
HttpContext ctx = app.Context;
// 检测请求是否是刷新
RefreshAction.Check(ctx);
return ;
}
}
}
检测请求是否是刷新请求,就是通过比较判断页面标签和最后一次服务标签大小,而页面标签存储在页面隐藏域中,最后一次服务标签从Hashtable中得到。
{
// 初始化标签号
EnsureRefreshTicket(ctx);
// 浏览器缓存中得到最后一次服务标签
int lastTicket = GetLastRefreshTicket(ctx);
// 从隐藏域中得到页面标签
int thisTicket = GetCurrentRefreshTicket(ctx, lastTicket);
// 比较页面标签和最后一次服务标签的大小
if (thisTicket > lastTicket ||
(thisTicket == lastTicket && thisTicket == 0 ))
{
// 更新服务标签
UpdateLastRefreshTicket(ctx, thisTicket);
// 写入Items标记不是刷新
ctx.Items[PageRefreshEntry] = false ;
}
else
{
// 判断本次回发是刷新
ctx.Items[PageRefreshEntry] = true ;
}
}
将判断的信息交给Page类,这个类是要被页面继承的,那么我们就可以在.cs文件里得到本次请求是否为刷新,就先像用IsPostBack属性一样。另外会自动将HttpContext.Current.Item中存储的页面标签存储到页面隐藏域中去
{
#region New Properties
// 指示是否为刷新
public bool IsRefreshed
{
get
{
object o = HttpContext.Current.Items[RefreshAction.PageRefreshEntry];
if (o == null )
return false ;
return ( bool )o;
}
}
#endregion
#region Overrides
protected override void OnPreRenderComplete(EventArgs e)
{
base .OnPreRenderComplete(e);
SaveRefreshState();
}
#endregion
#region Helpers
///
/// 将下一个页面标签存储到隐藏域中
///
private void SaveRefreshState()
{
int ticket = ( int )HttpContext.Current.Items[RefreshAction.NextPageTicketEntry];
ClientScript.RegisterHiddenField(RefreshAction.CurrentRefreshTicketEntry,
ticket.ToString());
}
#endregion
}
在让页面继承该类,并在页面中判断是否刷新
{
protected void Page_Load( object sender, EventArgs e)
{
if ( ! IsPostBack)
{
Loaddata();
}
}
private void Loaddata()
{
SqlConnection con = new SqlConnection( " Data Source=.\\;Initial Catalog=db_HttpModule;User ID=sa;Password=asd123 " );
DataTable dt = new DataTable();
SqlDataAdapter sqldataadaper = new SqlDataAdapter( " select * from dt_info " , con);
sqldataadaper.Fill(dt);
GridView1.DataSource = dt;
GridView1.DataBind();
}
protected void btn_OK( object sender, EventArgs e)
{
if ( ! IsRefreshed)
{
SqlConnection con = new SqlConnection( " Data Source=.\\;Initial Catalog=db_HttpModule;User ID=sa;Password=asd123 " );
SqlCommand com = new SqlCommand( " Insert into dt_info(name,age) values(' " + txtname.Text + " ',' " + txtage.Text + " ') " , con);
con.Open();
com.ExecuteNonQuery();
con.Close();
}
Loaddata();
txtage.Text = "" ;
txtname.Text = "" ;
}
}
那么我们此时再来运行与上面一样的界面,就不会发生刷新页面增加数据的问题了。
本文知识来源:《ASP.NET2.0 高级编程》
程序环境:VS2008+SQL 2005
注意自己将.mdf文件附加到数据库中去,并修改一下数据库连接字符串。
本文程序下载