一次MOSS开发中iFrame表单提交的古怪问题解决
微软的.NET Framework 3.5自带了Ajax框架,将以往传统的ASP.NET开发带入了一个全新的Ajax.NET开发时代,我们除了在页面上引入ScriptManager控件用以在客户端注册功能丰富的Ajax框架脚本外,这个庞大的框架还提供了诸多功能强大的Ajax控件,例如著名的UpdatePanel、ModalPopupExtender、Rating等控件。Ajax框架和控件的引入大大简化了开发人员的开发任务,同时也给用户带来了全新的Web体验,但是我们在使用复杂的框架提供的脚本时也常常会遇到这样或那样的问题,有很多问题相信不少开发人员都能独立解决,不过有些复杂的问题还真是很伤脑筋。
本来在MOSS中使用Ajax开发就已经不是一件轻易的事情,或许高手们觉得这没有什么,是的!我们在Google上会搜到很多介绍这方面的文章,而且配置步骤都写得非常详细,按照前辈们的经验,只要认真按照步骤将环境配置好,一般都是没有什么问题的,在MOSS中开发Ajax应用程序就如同简单的Ajax网页一样,只是部署的时候稍微要麻烦一些。这里我不想详细讲解在MOSS中如何进行Ajax开发,只是想说一说前段时间在MOSS开发中因为Ajax框架所引起的一个非常怪异的问题,一直困扰了我好几天,不过最终算是委曲求全得找到了一个替代的解决办法,至于会不会引起其它的什么问题,读者也可以帮我分析一下。
前不久我写了一篇有关在FireFox中通过脚本获取客户端本地所选文件路径的文章http://www.cnblogs.com/jaxu/archive/2009/04/19/1439016.html,里面介绍了通过客户端上传文件时如果通过javascript得到文件的本地路径,事实上,在真正的文件上传过程中,得到文件的客户端路径意义是不大的,除非我们需要实现如图片本地预览的功能,否则我们一般都可以通过Form的Post方法得到要上传的文件,在C#一般都是这样的。
<form id="form1" runat="server" method="post" enctype="multipart/form-data">
<input id="File1" name="mtfile" type="file" />
<asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
</form>
</body>
{
HttpFileCollection files = Request.Files;
if (files != null && files.Count > 0)
{
for (int i = 0; i < files.Count; i++)
{
// TODO something
}
}
}
设置Form的method属性为post,并设置enctype为mulipart/form-data,当页面提交时,在服务端通过Request.Files方法即可得到上传文件的对象集合。非常简单,我们根本不需要在客户端通过javascript得到文件的路径。不过这里有一个限制,那就是页面必须post到服务端才能得到要上传的文件,也就是说,我们不能通过javascript方式在页面无刷新的情况下将文件上传到服务器,这也是Ajax唯一不能做到的一件事情。不过我们通过一个比较老旧的技术可以避开这个问题,那就是在页面上使用隐藏的iFrame,在页面提交前将Form的target指向这个隐藏的iFrame,页面提交时iFrame会被刷新提交,从而避免了整个页面被刷新。
事实上,在Ajax兴起前,很多“无刷新”的页面几乎都是通过这种方式来实现的,iFrame可以提交数据,而且还避免了网页的整体刷新。在Ajax兴起后,iFrame似乎很少再被人们提起,但是有一个例外,那就是文件上传!我们可以去当今比较流行的网站考察一下,像163邮箱、Gmail等,都无一例外地使用了iFrame上传文件。我们可以将上面代码中的HTML部分稍作修改就可以实现使用iFrame上传文件的功能。
<form id="form1" runat="server" target="ifu" method="post" enctype="multipart/form-data">
<iframe frameborder="0" id="ifu" name="ifu" style="display: none;"></iframe>
<input id="File1" name="mtfile" type="file" />
<asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
</form>
</body>
后台代码不变,只是在Form上加了一个target属性,用来指向iFrame,当页面提交时会自动提交iFrame对象,而不会将Form本身提交。当遇到页面上还有其它表单需要提交时,我们可以这样做:先在提交按钮的客户端事件上将Form的target指向隐藏的iFrame,然后返回True提交表单,这时iFrame会被提交;在服务端处理完数据保存后注册一段脚本,用来将iFrame的父页面中Form的target改回自身。这样就可以模拟一次iFrame提交而不会影响到页面上其它的功能,我们只是在页面需要被提交时才去修改Form的target属性,提交完后再改回来。
这看起来似乎是一个很不错的主意。看看代码吧!
<form id="form1" runat="server" method="post" enctype="multipart/form-data">
<iframe frameborder="0" id="ifu" name="ifu" style="display: none;"></iframe>
<input id="File1" name="mtfile" type="file" />
<asp:Button ID="Button1" onclientclick="document.forms['form1'].target = 'ifu';return true;" runat="server" Text="Button" OnClick="Button1_Click" />
</form>
</body>
{
HttpFileCollection files = Request.Files;
if (files != null && files.Count > 0)
{
for (int i = 0; i < files.Count; i++)
{
//TODO something
}
}
string script = "alert('{0}');window.parent.document.forms['form1'].target = '_self';";
ClientScript.RegisterClientScriptBlock(this.Page, this.GetType(), string.Empty, string.Format(script, "Save Successfully!"), true);
}
<ContentTemplate>
<input id="File1" name="mtfile" type="file" />
<asp:Button ID="btSave" runat="server" Text="Save" onclick="btSave_Click" />
</ContentTemplate>
<Triggers>
<asp:PostBackTrigger ControlID="btSave" />
</Triggers>
</asp:UpdatePanel>
这将导致页面回传,UpdatePanel控件的意义也就失去了。在页面上放置隐藏的iFrame,按照前面介绍的方法通过javascript动态去修改Form的target属性,提交iFrame,可以实现类似于Ajax方式的文件上传功能,其实页面同样被刷新了,只是刷新的是隐藏的iFrame,用户不会有什么感觉。
前面说了这么多,只是想说说我所遇到的问题的背景,现在步入正题!
在MOSS中开发页面和普通的ASP.NET页面基本没有什么不同,主要就是部署的时候会有一些麻烦。那么,按照前面介绍的方法将编写好的页面部署到站点上,运行时我发现了一个奇怪的问题,那就是第一次按钮触发事件的时候服务端可以正确响应,并且是通过iFrame提交过来的,但是从第二次开始就需要等待十几秒的时间按钮才能再次被触发。一开始我以为是iFrame在被提交后没有响应完毕,来不及处理第二次请求,后来通过设置断点和插入调试脚本进行测试,发现iFrame已经完全响应完毕,按钮还是不能被点击(这里说的按钮不能被点击是指Button不能响应服务端事件)。
究竟发生了什么问题?
在.NET 1.1时代,我们通常会遇到按钮的事件丢失等问题,但这是在.NET 3.5的环境下,根本不存在这种问题,况且按钮在第一次的时候是可以被点击的,程序一直处于运行状态,没有人修改过代码,让我非常奇怪!这个问题我反复调试并采用了很多不同的方法去尝试,但是问题依旧,如查看页面上其它部分可能导致的脚本干扰、setInterval方法的使用是否会导致程序处于等待状态(事实上这个根本不可能)、去掉所有可能导致此问题的控件和代码等等...天啊!我几乎尝尽了所有能够想到的办法,但是这块大石头依然纹丝不动,我崩溃了!!
过了一个周末,在家睡了两天,脑海中一直想的就是究竟是什么原因导致了按钮的事件不能被触发,我也尝试过在FireFox下利用FireBug跟踪按钮的客户端代码执行情况,没有什么结果。周一上班的时候突然想到用排除法来验证一下,看看究竟是哪部分代码出现了问题。因为之前我在本地创建的工程中使用了iFrame提交表单,并且利用javascript在页面往返服务器的过程中动态修改了Form的target属性,并没有发现按钮事件不能被触发的问题,说明问题不是出在我所写的代码中。我在MOSS站点中创建了一个功能一样的页面,上面只有非常简单的几行代码,然后编译、部署、激活特定的Feature、访问页面,简单看了一下,功能很正常,说明这种方法在MOSS下是可以正常使用的,并没有之前假象的会受到MOSS本身机制的影响。
生产环境中的页面要稍微复杂一些,里面除了一些必须的功能和UserControl外,整个页面是继承自一个公共的模板页,难道问题出在模板页上?我又仔细看了看模板页中的代码,几乎尝试着将模板页中所有的控件都删除了,但是问题依然没有解决,一身冷汗啊,一上午的时间就这么让我浪费了。做过MOSS项目的朋友可能会比较清楚,在MOSS上开发项目复杂的并不是如何去写代码,而是部署和调试,经常大把的时间都浪费在这个上面,更何况我为了测试这个问题产生的原因还要新建页面重新部署站点,然后调试代码,光这个过程就比较繁琐了。
反正已经开始做了,午饭过后,我打算彻底搞定它。问题既然不是出在页面本身,那一定是出在模板页上,因为之前没加模板页的时候是可以的,后来将页面继承自模板页后问题就来了。在FireFox中查看页面的源代码,仔细查看生成的HTML和脚本,发现在Body和Form标签上有两个脚本事件,不知道是干什么用的,很好奇,问了一下老大,他说这是MOSS在新建模板页时自动加上的,没有谁刻意去加它。代码片段如下:
<form id="Form1" runat="server" onsubmit="return _spFormOnSubmitWrapper();" method="post" enctype="multipart/form-data">
我尝试着将这两个事件取消掉,然后重新部署运行程序,哈哈!终于可以了。那个按钮的事件再也没有丢失过,可以一直被点击,而不会出现不响应的情况。其实罪魁祸首的就是form的onsubmit事件中的_spFormOnSubmitWrapper方法,取消它就可以解决问题。
但是问题马上又来了,既然这个事件是MOSS自动加上的,那肯定有它的用途,我们不能随意就将它删掉,说不定以后哪里就会出问题(虽然我到后来也不太清楚这个函数究竟是用来干什么的)。那么只能曲线救国了,用FireBug看看它的具体代码吧,顺便跟了一下。
function _spFormOnSubmitWrapper()
{
if (_spSuppressFormOnSubmitWrapper)
{
return true;
}
if (_spFormOnSubmitCalled)
{
return false;
}
if (typeof(_spFormOnSubmit)=="function")
{
var retval=_spFormOnSubmit();
var testval=false;
if (typeof(retval)==typeof(testval) && retval==testval)
{
return false;
}
}
RestoreToOriginalFormAction();
_spFormOnSubmitCalled=true;
return true;
}
这个方法只要返回true就会触发服务器端时间,如果返回false则不会触发。我反复看了一下,导致函数返回false的原因是因为_spFormOnSubmitCalled的值为true,那么我们只需要将这个变量的值设为false即可重新触发服务器端事件了。这个好办,我马上修改代码,在button按钮的客户端事件代码中这样写:
document.forms["aspnetForm"].target = "iframeHidden";
_spFormOnSubmitCalled = false;
return true;
然后服务端返回的时候再将form的target改回_self,这样就可以了!
我不知道MOSS自动加上的那个Form事件是用来干什么的,但至少我让_spFormOnSubmitCalled变量的值为false可以导致按钮的事件被触发,并且可以实现我预期的效果。因为我在页面提交成功后会整个刷新页面,所以也不用担心修改这个值后会带来什么样的后果。最后来看一下服务器端要注册的脚本。
window.parent.location.href += '#';
window.parent.location.reload();";
private const string scriptFailed = @"alert('{0}');
window.parent.document.forms['aspnetForm'].target = '_self';";
分为两种,如果成功则重新刷新整个页面,如果失败则修改父页面Form的target属性的值为_self。你可能会问我为什么要将父页面的location.href加上一个#,这主要是为了解决在FireFox下通过iFrame提交表单并重新刷新整个页面时出现是否重新提交数据的提示(这个问题在IE下不会出现),浏览器只认URL,我们稍微修改一下URL的内容,只要地址不变,重新刷新页面时就不会出现是否重新提交数据的提示了。
到目前为止,我将我的代码做了这样的修改,不知道会不会遇到什么问题。写这篇文章的目的有两个,一是记录一下自己解决这个问题的过程;二是想告诉各位正在做MOSS开发的朋友,如果遇到通过Ajax方式无法触发服务器端事件的问题时,不妨认真检查检查客户端生成的HTML和脚本,找找原因在哪里。
另外,如果有哪位朋友能详细讲解一下我所遇到的问题的具体原因以及_spFormOnSubmitWrapper函数的具体含义,也欢迎指正!