键盘敲击者cncxz

  博客园 :: 首页 :: 联系 :: 订阅 订阅 :: 管理
  34 Posts :: 0 Stories :: 282 Comments :: 14 Trackbacks

搜索

 
 

我的标签

最新评论

    执行过postback操作的web页面在刷新的时候,浏览器会有“不重新发送信息,则无法刷新网页”的提示,若刚刚执行的恰好是往数据库插入一条新记录的操作,点[重试]的结果是插入了两条重复的记录,以前一直是用保存数据后重新转向当前页面的方法解决,最近又找到了一个新的方法。

问题分析

    在System.Web.UI.Page类中,有一个名为ViewState属性用以保存页面的当前视图状态,观察每个aspx页面最终生成的html代码可以发现,其实就是向页面添加了一个名为__VIEWSTATE的隐藏域,其value值就是页面的当前状态,每次执行postback过后,该value值都会发生变化,而刷新页面则不会改变。

    针对这种情况,我们可以在页面代码执行的末尾将当前的ViewState写到一个Session中,而在页面加载时则判断该Session值是否与当前ViewState相等(其实Session值恰好是ViewState的前一状态),若不等,则是正常的postback,若是相等则是浏览器刷新,这样一来,只要在我们的数据插入代码外嵌套一个if判断就可以达到防止数据重复提交的目的了。

    其实到这里问题还没有完全解决,具体说来就是Session的键值问题。假设我们将ViewState保存为this.Session["myViewState"],如果一个用户同时打开两个防刷新提交的页面就乱套了,那针对页面的url设置Session的键值呢?还是不行,因为用户有可能在两个窗口中打开同一页面,所以必须为每次打开的页面定义唯一的Session键值,并且该键值可以随当前页面实例一起保存,参考ViewState的保存方式,我们直接向页面添加一个隐藏域专门存放Session键值就可以了。
 
    经oop80和Edward.Net的提醒,为了尽可能地降低Session数据对服务器资源的占用量,现将上述方案略做调整,将ViewState利用md5加密后返回的32位字符串写入Session。

    另外,由于本方法会生成额外的Session占用服务器资源,所以请在必须保留当前页面状态的情况下使用,若无需保留当前页面状态,则在完成数据提交后直接重定向到当前页面即可。

SubmitOncePage

    SubmitOncePage是针对上述分析写的一个继承自System.Web.UI.Page的基类,需要防止刷新重复提交数据的页面从该基类继承,源码如下:

namespace myControl
{
    
/// <summary>
    
/// 名称:SubmitOncePage 
    
/// 父类:System.Web.UI.Page
    
/// 描述:解决浏览器刷新造成的数据重复提交问题的page扩展类。
    
/// 示例:    if (!this.IsRefreshed)
    
///            {
    
///                //具体代码
    
///            }
    
///    原创:丛兴滋(cncxz)    E-mail:cncxz@126.com
    
/// </summary>

    public class SubmitOncePage:System.Web.UI.Page
        
{
            
private string _strSessionKey;
            
private string _hiddenfieldName;
            
private string _strLastViewstate;
           
            
public SubmitOncePage()
            
{
                _hiddenfieldName 
= "__LastVIEWSTATE_SessionKey";
                _strSessionKey 
= System.Guid.NewGuid().ToString();
                _strLastViewstate 
= string.Empty;
            }


            
public bool IsRefreshed
            
{
                
get
                
{
                    
string str1 = GetSessinContent();
                    _strLastViewstate 
= str1;
                    
string str2 = this.Session[GetSessinKey()] as string;
                    
bool flag1 = (str1 != null&& (str2 != null&& (str1 == str2);
                    
return flag1;
                }

            }


            
protected override void Render(System.Web.UI.HtmlTextWriter writer)
            
{
                
string str = GetSessinKey();
                
this.Session[str] = _strLastViewstate;
                
this.RegisterHiddenField(_hiddenfieldName, str);
                
base.Render(writer);
            }



            
private string GetSessinKey()
            
{
                
string str = this.Request.Form[_hiddenfieldName];
                
return (str == null? _strSessionKey : str;
            }


            
private string GetSessinContent() {
                
string str = this.Request.Form["__VIEWSTATE"];
                
if (str == null{
                    
return null;
                }

                
return System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(str, "MD5");
            }

           
        }

}

测试项目

    首先将SubmitOncePage类的源码编译成一个单独的dll,然后进行测试,步骤如下:

    1、新建一个asp.net web应用程序;
    2、添加SubmitOncePage类对应的dll引用;
    3、给webform1添加一个Label控件(Label1)和一个Button控件(Button1);
    4、设置Label1的Text为0;
    5、双击Button1转到codebehind视图;
    6、修改类WebForm1的父类为SubmitOncePage并添加测试代码,结果如下:

public class WebForm1 : myControl.SubmitOncePage
    
{
        
protected System.Web.UI.WebControls.Label Label1;
        
protected System.Web.UI.WebControls.Button Button1;    
        

        
Web 窗体设计器生成的代码

        
private void Button1_Click(object sender, System.EventArgs e)
        
{
            
int i=int.Parse(Label1.Text)+1;
            Label1.Text 
= i.ToString();
            
if (!this.IsRefreshed) 
            
{
                WriteFile(
"a.txt", i.ToString()); 
            }

            WriteFile(
"b.txt", i.ToString());  
        
            
        }


        
private void WriteFile(string strFileName,string strContent)
        
{
            
string str = this.Server.MapPath(strFileName);       
            System.IO.StreamWriter sw 
= System.IO.File.AppendText(str);
            sw.WriteLine(strContent);
            sw.Flush();
            sw.Close();  
        }

    }

    7、按F5运行,在浏览器窗口中连续点击几次Button1,然后刷新几次页面,再点击几次Button1;
    8、转到测试项目对应目录下,打开a.txt和b.txt文件,可看到if (!this.IsRefreshed) 的具体效果。

相关文件下载
http://files.cnblogs.com/cncxz/SubmitOncePage.rar         最后更新于 2005-12-28





posted on 2005-12-25 09:54 cncxz(虫虫) 阅读(7051) 评论(31) 编辑 收藏

Feedback

为什么不用数据库的手段,比如,设置一个Unique Index?
 回复 引用   
#2楼[楼主]2005-12-25 13:15cncxz(虫虫)      
To yann:主要是因为提交的数据不一定都是插入数据库,比如说外发个邮件之类的,所以参考 if (!this.IsPostBack)的方式定义了 if (!this.IsRefreshed)

 回复 引用 查看   
#3楼2005-12-25 14:06dudu      
好方法!
 回复 引用 查看   
#4楼2005-12-25 15:01flyye_cs      
不错...
 回复 引用 查看   
#5楼2005-12-25 16:19高海东      
不错
 回复 引用 查看   
#6楼2005-12-25 19:36Nick Yao      
我在MSDN上见到另一个方法~~
不过好象没你的简单~~
不错,先收藏!!!
 回复 引用 查看   
#8楼2005-12-25 22:02Edward.Net      
希望不要保存到Session中去,在中小型项目中还可接受,如果在大型项目中并发量大页面访问比较多而且页面数据相对较多的情况下很容易会造成服务器内存很快就会被耗尽的,应该尽量减少这种无效的冗余数据的使用。现在我们公司的产品就是由于在使用Session的时候没有认真考虑,现在登陆以后一直到需要测试的页面aspnet_wp进程占用的内存就会达到100M左右,更别说投入正是使用了。现在客户那里3G的内存都感觉很吃劲。
 回复 引用 查看   
#9楼2005-12-26 11:11oop80      
这样的方法有问题的,
第一个是内存占用,你这样做,在正常操作环境下,会产生大量的废session,单用户可能感觉不到,但是一旦很多用户同时操作的时候,那就相当可观了。
第二,效率问题。如果viewstate很大,而不幸的又多按了几次f5,那么你的服务器就又多点事情要做了,想想看,比较两个100k的相同文本,会消耗服务器多少的资源,如果这个viewstate有1m呢?当然,在正常的操作下面(没有按f5),比较的效率应该还是可以的。
第三,兼容性问题,现在ms的viewstate是这么设计的,但是万一以后变掉了,f5以后viewstate还是会变,那怎么办?况且,目前viewstate在f5情况下不变,也是你试验出来的吧,似乎没有官方的说法可以证实这一点,这也是需要考虑的地方
 回复 引用 查看   
防止反复提交有这么麻烦吗?有吗?没有吗?
 回复 引用   
#11楼2005-12-28 13:28汤智程      
不错
 回复 引用 查看   
点第一次按钮的时候可以操作,再继续点一次按钮,却认为我刷新,你的这个有问题!!!!!
 回复 引用   
#13楼[楼主]2006-01-08 09:48cncxz(虫虫)      
To wy :不知道你的具体代码是怎么样的,看样子这个方法应该是某种情况下不适用,你可以看看msdn上的这篇文章http://www.microsoft.com/china/msdn/library/webservices/asp.net/BedrockAspNet.mspx
 回复 引用 查看   
msdn上这篇是可行的。其实很多时候重复提交是有用的,比如说网速慢导致登录失败的时候。
 回复 引用 查看   
我今天都因为这个问题苦恼了一天了,总体上我还是觉得好的!^_^
 回复 引用   
#16楼2006-04-14 11:37高海东      
可以实现功能就好,以后可以找更好的方法
 回复 引用 查看   
还是提交后重定向得了
 回复 引用   
#18楼2006-05-10 16:53Sunny      
点了按钮之后使用javascript把按钮disable掉,然后当服务器处理完之后,在服务器端重新设置按钮为enable不就可以了吗?

有这么麻烦吗???
 回复 引用 查看   
#19楼[楼主]2006-05-11 09:28cncxz(虫虫)      
@Sunny
disable按钮只是防止用户在数据提交过程中再次点击造成数据重复,不能防止数据提交成功后刷新当前页面造成的数据重复
 回复 引用 查看   
服了你们,,这么简单的问题,,在程序最后调用本身页面一次就不会被刷新了。
 回复 引用   
呀呀.....
同志们呀,c#呀asp.net呀都学得不错呀.
为什么不借助一下html和js里的基本的东西呢?f5也是可以禁止的呀,并且简单的要命呀!!
郁闷!!!!
一个个都学成呆子了!!!!!
有人硬是要点浏览器上的刷新按钮那有什么办法,麻烦是人搞的.
不过呢,有人肯花心思把这样的问题多加考虑并且能干的解决掉确属难得...
多多支持一下我们的大楼主!!!!!
 回复 引用   
好像有点道理
 回复 引用   
@小林子
一语惊醒我这呆子呀!!谢谢了!!
 回复 引用   
把验证部分的代码改成下面的样子试试看。
//int i = int.Parse(Label1.Text) + 1;
//Label1.Text = i.ToString();
if (!IsRefreshed)
{
WriteFile("a.txt", IsRefreshed.ToString());
}
else
{
WriteFile("b.txt", IsRefreshed.ToString());
}
 回复 引用   
个人还是觉得 用JS制止 刷新的方法比较好,如果客户把JS禁止了,那是客户自己找的问题
 回复 引用   
@小林子
所以像你这种人..不客气地说,以你这种态度,你永远最多只能拿几K的工资.上万就难了...
知其然知其所以然,这是一种学习态度..是思想的进步..不是只会copy code 的 coder..
如有不妥,见谅! :(
 回复 引用   
上面的方式有问题,有时候ViewState是不会变化的,其实很简单,我将代码修改成如下形式就可以了,也就是常见的令牌方式:
public partial class BasePage : System.Web.UI.Page
{
private string _strSessionKey;
private string _hiddenfieldName;
private string _strLastViewstate;

protected void Page_Load(object sender, EventArgs e)
{

}

public BasePage()
{
_hiddenfieldName = "__LastVIEWSTATE_SessionKey";
_strSessionKey = System.Guid.NewGuid().ToString();
_strLastViewstate = string.Empty;
}

public bool IsRefreshed
{
get
{
/*
string str1 = GetSessinContent();
_strLastViewstate = str1;
string str2 = this.Session[GetSessinKey()] as string;
bool flag1 = (str1 != null) && (str2 != null) && (str1 == str2);
return flag1;
* */

string str1 = GetRequestKey();
string str2 = GetSessinKey();
if (str1 == str2)
{
return false;
}
else
{
return true;
}
}
}

protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
/*
string str = GetSessinKey();
this.Session[str] = _strLastViewstate;
this.RegisterHiddenField(_hiddenfieldName, str);
base.Render(writer);
* */

this.RegisterHiddenField(_hiddenfieldName, _strSessionKey);
this.Session[_hiddenfieldName] = _strSessionKey;
base.Render(writer);
}

private string GetRequestKey()
{
string str = this.Request.Form[_hiddenfieldName];
return (str == null) ? _strSessionKey : str;
}

private string GetSessinKey()
{
/*
string str = this.Request.Form[_hiddenfieldName];
return (str == null) ? _strSessionKey : str;
* */
object sessionKey = Session[_hiddenfieldName];
if (sessionKey == null)
{
return _strSessionKey;
}
else
{
return sessionKey.ToString();
}

}
/*
private string GetSessinContent()
{
string str = this.Request.Form["__VIEWSTATE"];
if (str == null)
{
return null;
}
return System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(str, "MD5");
}
* */
}
 回复 引用   
本群是.NET开发人员的交流空间,欢迎广大.NET开发者来我们群一起讨论,研究.NET技术。(凑热闹的、其他语言开发人员以及喜欢涉及多门语言的误扰!)专业研究,探讨.NET技术的群:12339353.
 回复 引用   
#29楼2007-11-05 12:57      
@小林子

这位同学说话很搞笑。。没有办法就不解决了?那么银行在线支付不全部完蛋?银行不全部倒闭了?
 回复 引用 查看   
开发中确实遇到这样的问题,不过楼主的只能参考,或者是临时的解决方案。个人觉得,作NET开发,一定要对NET框架的运行机制多多了解才行。
 回复 引用   
#31楼2009-11-29 10:27巴山      
string str = this.Request.Form["__VIEWSTATE"];
这里为什么不用DateTime.Now.Ticks.ToString()呢?这样刷新时,值也会改变的。
 回复 引用 查看