题记:
在处理页面事件时,我们会经常会碰到这样的情况:当我们在提交一项页面表单时,在提交成功后,当我们试图按F5刷新页面时,数据会再次的被重复提交。那么 asp.net应用应用服务器是无法区别这是正常点击按钮添加还是f5刷新添加,那么这样就会导致在数据库里会存在n 条一莫一样的数据。为什么在原Asp开发程序中不会碰到这样的问题呢?我觉得是因为Asp程序主要都是将表单提交给另外一个页面处理,并且,这个页面处理 之后,将跳转到另外一个提示页面。那么在Asp程序中,只需要在回退时将页面设置为过期那么就可以有效的避免重复提交的问题。但是在Asp.Net中,基 本上所有的操作都是基于事件操作,而事件的本质上就是页面自己提交给自己,并且页面无法识别提交时正常操作还是重复刷新。
在我开发的第一个Web应用程序中是在项目后期,也就是基本上快完成时发现了这个问题,项目后期整个的页面框架都已经非常稳定,这时候来修改整个框架是得
不偿失的,只能采取变通的方式解决这个问题。幸好整个系统的数据访问层全部都是通过存储过程来处理数据的,那么我们根据几个基本的关键字段判断这条数据是
否在表里存在,如果存在那么就抛出异常(主要是Insert的存储过程),通过存储过程的判断来判断这条数据是否是重复提交的数据。这个只是权宜方案,并
不是一个很好的方案,因为这样只对实体表有用,但是对关系表的插入可能很难判断,关系表主要存储的是别的表的主键,如果多个他表主键在逻辑上形成了唯一
性,这样还比较好判断,但是如果多个他表主键不能在逻辑上形成唯一性,那么对唯一性的判断将十分的困难。并且实体表在某些情况下也无法适用,如果某个实体
表允许除主键字段,其他字段都允许相同,那么这个也是无法判断是否是重复提交的数据。
那么如何解决这个问题呢?在微软Msdn上提供了一套解决方案,这个方案是意大利的Dino Esposito提供的。他的思想就是在客户端保存一个标志,在服务端也保存一个标志,在提交时对比两个标志的值,来判断是否是重复提交。
先看下面代码,首先是一个RefreshAction静态类,这个类主要是用来初始化服务端Session保存上一次票证的值并且对比客户端和服务端票证的值,当检测到刷新不是重复刷新时,将要把客户端的票证值更新到服务端
[c-sharp] view plaincopy
- public static class RefreshAction
- {
- // 常量
- //服务端票证key
- public const string LastRefreshTicketEntry = "__LASTREFRESHTICKET";
- //客户端票证key
- public const string CurrentRefreshTicketEntry = "__CURRENTREFRESHTICKET";
- //用来保存是否是重复刷新的属性的key
- public const string PageRefreshEntry = "IsPageRefresh";
- private static Hashtable requestHistory = null; //存储请求历史
- // 检测F5按钮是否被按下
- public static void Check(HttpContext ctx)
- {
- //初始化服务端票证
- EnsureRefreshTicket(ctx);
- //从Session里读取上一次提供的票证
- int lastTicket = GetLastRefreshTicket(ctx);
- //从请求里的隐藏域里读取当前页面的票证
- int thisTicket = GetCurrentRefreshTicket(ctx);
- // 对比两个票证
- if (thisTicket > lastTicket ||
- (thisTicket == lastTicket && thisTicket == 0))
- {
- //如果当前的票证值大于上一次的票证值 或者
- //当前票证值等于上一次票证值,并且当前票证值为0(这是第一次刷新)
- //那么更新Session里上一次的票证值为当前票证值
- UpdateLastRefreshTicket(ctx, thisTicket);
- //设置当前页是否重复刷新属性为false
- ctx.Items[PageRefreshEntry] = false;
- }
- else
- {
- //设置当前页是否重复刷新属性为true;
- ctx.Items[PageRefreshEntry] = true;
- }
- }
- //确认上一次的票证不为空值
- static void EnsureRefreshTicket(HttpContext ctx)
- {
- if (requestHistory == null)
- requestHistory = new Hashtable();
- }
- //得到上一次请求的票证值
- static int GetLastRefreshTicket(HttpContext ctx)
- {
- if (!requestHistory.ContainsKey(ctx.Request.Path))
- return 0;
- else
- return (int) requestHistory[ctx.Request.Path];
- }
- //从当前请求里的到隐藏域里保存的当前票证值
- static int GetCurrentRefreshTicket(HttpContext ctx)
- {
- return Convert.ToInt32(ctx.Request[CurrentRefreshTicketEntry]);
- }
- // 将当前的票证值保存到Session里的上一次刷新的票证值
- private static void UpdateLastRefreshTicket(HttpContext ctx, int ticket)
- {
- requestHistory[ctx.Request.Path] = ticket;
- }
- }//end class
下面是一个HttpModule类,在请求开始时就来检测双方的票证值
[c-sharp] view plaincopy
- public class RefreshModule : IHttpModule
- {
- public RefreshModule()
- {
- //
- // TODO: Add constructor logic here
- //
- }
- #region IHttpModule Members
- public void Dispose()
- {
- throw new NotImplementedException();
- }
- public void Init(HttpApplication app)
- {
- //注册请求关联状态时的事件处理器,就是说当一个请求到达服务器,
- //那么首先触发这个事件,由OnAcquireRqeustState事件处理
- app.AcquireRequestState += new EventHandler(this.OnAcquireRequestState);
- }
- #endregion
- private void OnAcquireRequestState(object sender, EventArgs e)
- {
- HttpApplication app = sender as HttpApplication;
- HttpContext ctx = app.Context;
- RefreshAction.Check(ctx); //RefreshAction类来检查当前请求的上下文 Rey
- return;
- }
- }//end class
下面是继承于Page页面的基类,它主要用来保存刷新的次数和客户端票证的值,并且提供一个属性来标志此页面是否是重复提交的页面
[c-sharp] view plaincopy
- public class MyBasePage:System.Web.UI.Page
- {
- // 常量
- public const string RefreshTicketCounter = "RefreshTicketCounter";
- public MyBasePage()
- {
- this.PreRender += new EventHandler(RefreshPage_PreRender);
- }
- //标志页面是否按F5进行重复刷新的标志属性
- public bool IsPageRefresh
- {
- get
- {
- object o = HttpContext.Current.Items[RefreshAction.PageRefreshEntry];
- if (o == null)
- return false;
- return (bool)o;
- }
- }
- //增加刷新票证的内部计数器
- public void TrackRefreshState()
- {
- //初始化刷新计数器
- InitRefreshState();
- //将刷新计数器加1,然后放进Session
- int ticket = Convert.ToInt32(Session[RefreshTicketCounter]) + 1;
- Session[RefreshTicketCounter] = ticket;
- }
- //初始化刷新计数器
- private void InitRefreshState()
- {
- if (Session[RefreshTicketCounter] == null)
- Session[RefreshTicketCounter] = 0;
- }
- // PreRender事件处理器
- private void RefreshPage_PreRender(object sender, EventArgs e)
- {
- //在页面呈现之前就保存票证值到隐藏域
- SaveRefreshState();
- }
- //创建隐藏域来保存当前请求的票证值
- private void SaveRefreshState()
- {
- //将票证计数器的值加1,然后将此值注册到当前票证隐藏域中
- int ticket = Convert.ToInt32(Session[RefreshTicketCounter]) + 1;
- this.ClientScript.RegisterHiddenField(RefreshAction.CurrentRefreshTicketEntry, ticket.ToString());
- }
- }//end class
下面是一个继承于MyBasePage类的页面,它通过判断是否是重复刷新属性来显示相应的值
[c-sharp] view plaincopy
- public partial class repeatsubmit_add : MyBasePage
- {
- protected void Page_Load(object sender, EventArgs e)
- {
- }
- protected void Button1_Click(object sender, EventArgs e)
- {
- if (this.IsPageRefresh)
- {
- this.Label1.Text = "这是重复刷新的页面";
- }
- else
- {
- this.TrackRefreshState();
- this.Label1.Text = "这是正常提交的页面";
- }
- }
- }
在WebConfig的system.web节点下加入处理请求的HttpModule
[xhtml] view plaincopy
- <httpModules>
- <add name="MyModule" type="RefreshModule"/>
- </httpModules>
上面的代码就是解决重复刷新的代码,那么我们来分析这个代码,当我们进入页面然后点击Button1是怎么来处理这些刷新的信息的。
当我们进入页面时按照下面的顺序来执行
1、当第一次进入页面,首先由系统自动调用RefreshModule的Init事件,在此事件里,我们给Application对象的请求关联状态事件
(AcquireRequestState)注册了一个事件处理器(OnAcquireRequestState),那么当我们请求关联状态时会自动调用
OnAcquireRequestState函数。
2、调用MyBasePage的构造函数,在此函数里注册了PreRender的事件处理器。
3、第一次进入页面也是一个关联请求,现在会自动调用OnAcquireRequestState事件处理器
4、在OnAcquireRequestState事件处理器中我们调用静态类RefreshAction静态类的Check方法,HttpContext作为参数传入
1. 在Check方法里,我们首先初始化服务端票证(保存在Session里),让服务端票证的值为0。
然后我们得到上一次刷新的票证值也就是服务端票证值,它为0。
2. 我们得到这次请求保存在隐藏域里的当前的票证值,因为这是第一次请求,那么这个值为空,转换为整数,为0
3. 对比两个票证,如果当前的票证值大于上一次的票证值或者当前票证值等于上一次票证值,并且两者都为0(这表明是第一次
刷新),那么我们将当前的票证值保存为上一次票证值。这时候,客户端和服务端的票证值都为0。将标志页面是否是重复刷新的值
设置为false。如果对比条件为假,那么设置重复刷新的值为 true。
4. OnAcquireRequestState事件处理完毕
5、现在触发了PreRender事件,在页面呈现之前触发,此事件调用SaveRefreshState用来保存客户端当前票证的值。
1. 首先将刷新次数的值得到并加1,此时刷新次数为0。
2. 将刷新次数的加1的值保存到客户端当前票证的隐藏域中,那么现在当前票证的值为1,上一次票证值为0,刷新次数的值为0。
6、当我们点击Button1按钮的时候首先调用MyBasePage的构造函数注册PreRender的事件处理器
7、然后系统自动调用AcquireRequestState事件处理器,调用RefreshAction的Check方法
1. 初始化服务端票证函数无用,因为服务端票证已经存在值
2. 得到上一此票证刷新的值为0
3. 得到当前票证刷新的值为1
4. 判断票证,这时当前票证值是大于上次票证的值,将当前票证的值更新到上一次票证值,此时上一次票证值为1
5. 设置是否重刷新标志为false,这时候当前票证为1,上一次票证为1。
8、这时候不是调用PreRender事件,而是调用Button1的Click事件。
1. 判断MyBasePage的IsPageRefresh属性是否为真,很显然,现在这个值为假
2. 那么调用MyBasePage的TrackRefreshState方法,在这个方法里将刷新次数加1,保存在Session里。注意此时当前票证为1,上一
次票证为1,刷新次数为1。
9、那么这时候调用PreRender事件处理器的SaveRefreshState方法
1. 将刷新次数加1,并且保存到当前票证里,那么这时候当前票证为2,上一次票证为1,刷新次数为1。
那么我们可以观察到正常的提交服务端(上一次)票证始终小于客户端(当前)票证,刷新次数也小于当前票证,那么如果是按F5刷新呢
?我们观察一下代码
1、调用MyBasePage的构造函数注册PreRender事件
2、调用AcquireRequestState事件处理器里的Check方法
1. 初始化服务端票证,此时无效
2. 得到上一次刷新的票证为1,得到当前的票证也为1
3. 判断两个票证,此时肯定为假,那么设置重复刷新标志为false
3、处理button1的Click事件
判断IsPageRefresh属性,显然此时重复刷新标志为true,表明此次刷新是按F5刷新的。
在这里很奇怪,在正常点击时,当前票证(客户端)为2,上一次票证(服务端)为1,刷新次数为1,那么为什么按F5刷新以后,当
前票证为1了?
我刚开始也很奇怪,然后我做了一个实验,使一个按钮点击时增加隐藏域的值,让他加1,在Page_Load的时候去读取这个隐藏域,我
点击button让隐藏域的值增加,但是当我按F5时,隐藏域的值始终保持不变,那么我猜测,按F5时,不是将当前页面的数据提交给服
务端,是将缓存的数据提交给服务端,所以我们捕获到的数据值就是上一次正常提交的数据,此时隐藏域的值仍然保存最新的票证值
,但是按F5,这个值不会提交给服务器。,直到正常的点击Button1提交数据。
那么回退/前进可以说更好理解,我回退之后再点击Button1,此时提交的是上一个页面的隐藏域的值,但是存在Session里上一个票
证的值已经增加了,那么对比的时候就可以知道这是重复刷新提交的操作。
上面是这个解决方案件的原理已经阐述完毕。但是这个解决方案仍然有一定的缺点。如回退之后第一次点击可以探明是重复提交,但
是第二次点击仍然会说明是正常提交。还有一个缺点,服务端票证保存在Session里,Session是会过期的,这时候应该加一个
Session超时的判断。还有一个最大的缺点,此解决方案不能配合IFrame使用,因为在IFrame中,客户端页面会加载两次(即IFrame
外的父窗口和IFrame导向的子窗口),导致客户端票证与服务端票证相同,那么在IFrame中,提交始终是重复提交。
在实际应用中,我们肯定不能像示例那样使用这个解决方案。因为我们在项目中经常会使用用户控件,一般我们是将Button和文本框
包装成一个用户控件,点击Button抛出一个事件,由页面处理。这样还比较好判断页面是否是重复提交的。但是如果在Button在不抛
出事件,就在用户控件里自行解决,那么这样比较难以实现在事件中处理和判断页面是否重复提及。我认为这个判断最好放在
Page_Load事件里,如果是重复刷新的就跳转到另外一个提示页面(中断button的处理器),然后在跳转回来,作为第一次进入这个
页面。这样就可以避免在每次提交事件来做页面是否是刷新页面的判断。
在这里我觉得需要回顾一下Page的加载顺序。
1. Page的构造函数
2. protected void Page_PreInit(object sender, EventArgs e)
3. protected void Page_Init(object sender, EventArgs e)
4. protected void Page_InitCompleted(object send, EventArgs e)
5. protected void Page_PreLoad(object sender, EventArgs e)
6. protected void Page_Load(object sender, EventArgs e)
7. 处理完Page_Load事件,如果有提交事件就开始处理提交事件,在处理完提交事件之后在处理剩下的Page事件
8. protected void Page_LoadComplete(object sender, EventArgs e)
9. protected void Page_PreRender(object sender, EventArgs e)
10. protected void Page_PreRenderComplete(object sender, EventArgs e)
11. protected void Page_SaveStateComplete(object sender, EventArgs e)