我们要做的,只是在页面上添加一个控件而已。
不过,其实这只是个半成品,或者说是一个原型,但是很明显,我们做对了。:)
在实现上,我曾经在两个做法上斟酌了许久,第一种是继承ScriptManager,第二种则是提供一个新的控件。最终我选择了第二种方案,因为它能够避免和ScriptManager过渡耦合,在使用上也更加方便。
实现方式简析:
做这样一个控件的思路其实非常简单,那就是一个字:“骗”。你骗倒了客户端,再骗倒了服务器端,一切不就成了吗?
我把这个控件叫做AjaxFileUploadHelper。首先,它会输出一段JavaScript脚本,用来修改客户端的 PageRequestManager类。我保存了它用于提交请求的方法,并且使用相同的名字重写这个方法。在新提交方法中,首先判断页面中是否存在 <input type="file" />元素,如果不存在,则使用原有方法提交,否则就开始我们的提交逻辑,例如创建隐藏的iframe等等。
由于按照ASP.NET AJAX的实现,它是在Request Header里放入特殊的标记。我们如果要将数据POST到服务器端,则做不到这一点。因此,我们只能在客户端使用JavaScript创建< input type="hidden" />,以此作为特殊标记。页面中的AjaxFileUploadHelper会“尽快(但是总是要慢于ScriptManager)”检查 Request Body里的特殊标记,然后使用“反射”修改ScriptManager对象的属性,并且“弥补”一些因为它没有在“第一时间”做出反应而出现的问题。这 样,剩下的操作,ScriptManager就会认为它正在进行一个UpdatePanel刷新了。当然,我们可以在服务器端使用客户端上传的文件。
然后要做的就是使用自己的页面输出方法替换掉ASP.NET AJAX提供的页面输出方法,然后根据客户端能够识别的方式,重新提供输出。由于ASP.NET AJAX“封装”的过于完好,我甚至无法重新指定新的Content-Type(ASP.NET AJAX使用了text/plain作为Content-Type,再FireFox中直接用iframe显示则会出现一些问题),最后只能使用大量的反 射用于输出与客户端配套的JavaScript代码――没错,是JavaScript。谁让我们放弃了XMLHttpRequest呢,我们既然使用了 iframe就要放置一个页面了。
客户端的代码自然会响应iframe的onload事件,然后查找iframe里页面中有没有我们需要的JavaScript方法,如果没有,则说明出 现了错误,于是就要按照PageRequestManager的规则来“表现”错误。如果一切正常,则客户端就可以获得以前必须要从 XMLHttpRequest中才能获得的字符串。接着组成我们伪造的对象,交给原有的客户端方法去解析。剩下的,一切照旧。
JavaScript真的很容易骗,不像客户端代码,非要使用反射……
上面的描述听上去似乎很简单,不过在编写的控件中,一些细节方面的问题还是非常麻烦的。如果有机会,再让我慢慢道来。
目前控件还需要改进的地方:
目前控件只是一个半成品,它还有以下一些需要改进的地方:
-
实现了正常输出,但是如果异步刷新过程中服务器端出现了错误,我还没有为它实现异常情况下的输出。
-
现在的做法骗倒了框架,但是还没有骗倒开发人员。开发人员的一些操作,例如取消当前PostBack的功能还没有实现。细节方面还需要继续研究和实现。
-
用了许多反射,设法优化,例如对某些方法引入委托,或者尽可能多使用框架的公有成员。
-
现在随手的代码都是挤在一起,代码还需要进行重构。
控件的使用方式:
控件的使用非常简单,我们只需要在代码中
紧贴ScriptManager控件放置一个AjaxFileUploadHelper控件即可(这很重要,因为AjaxFileUploadHelper需要在第一时间让ScriptManager“认为”目前是部分刷新)。如下:
放置AjaxFileUploadHelper
<asp:ScriptManager ID="ScriptManager1" runat="server" ScriptMode="Debug" />
<jeffz:AjaxFileUploadHelper runat="server" ID="AjaxFileUploadHelper1" />
然后我们就可以随意在UpdatePanel内或外放置FileUpload控件了(当然,您自己写<input type="file" />也是可以的)。如下:
在UpdatePanel中使用FileUpload控件
<%= DateTime.Now %><hr />
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<%= DateTime.Now %><br />
<asp:FileUpload ID="FileUpload1" runat="server" /><br />
<asp:Button ID="Button1" runat="server" Text="上传" OnClick="Button1_Click" /><br />
您上传的文件为<asp:Literal runat="server" ID="message">0</asp:Literal>字节。
</ContentTemplate>
</asp:UpdatePanel>
与之对应的Code Behind代码是:
Jeffz.Web.ModalUpdateProgress
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
protected void Button1_Click(object sender, EventArgs e)
{
this.message.Text = this.FileUpload1.PostedFile.ContentLength.ToString();
}
}
我们来看一下使用效果。第一次打开页面时,页面上的两个时间相同:
选择文件,点击上传按钮之后:
一切就是这么简单!
我还会继续完善这个控件,但是可能需要过个几天才行。这周我会比较忙,可能不太再会去碰这个控件了。等控件成熟之后,我会详细分析一下这个控件的实现方式。
点击这里下载源代码。
PS:
这里向大家道个歉。本周的WebCast, 原计划是“全面讲解UpdatePanel的使用方式”,会涉及到从服务器端使用到客户端生命周期的方方面面。但是目前看来这个内容太多了。因此我会将其 拆分成两次,3/29的那次只会对UpdatePanel的服务器端使用作一个完整的讲解,并且会涉及到一些UpdatePanel的实现原理。而下一次 得课程,我将会对客户端的生命周期做一个全面的描述。
虽然分成了两次,但是我还是尽力保证了每次课程内容的充实性。