代码改变世界

UpdatePanel终于可以上传文件了!

2007-03-26 20:23  Jeffrey Zhao  阅读(38275)  评论(96编辑  收藏  举报

我们要做的,只是在页面上添加一个控件而已。

不过,其实这只是个半成品,或者说是一个原型,但是很明显,我们做对了。:)

在实现上,我曾经在两个做法上斟酌了许久,第一种是继承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“认为”目前是部分刷新)。如下:

<asp:ScriptManager ID="ScriptManager1" runat="server" ScriptMode="Debug" />
<jeffz:AjaxFileUploadHelper runat="server" ID="AjaxFileUploadHelper1" />

 

然后我们就可以随意在UpdatePanel内或外放置FileUpload控件了(当然,您自己写<input type="file" />也是可以的)。如下:

<%= 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代码是:

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();
    }
}

 

我们来看一下使用效果。第一次打开页面时,页面上的两个时间相同:

1

 

选择文件,点击上传按钮之后:

2

 

一切就是这么简单!

我还会继续完善这个控件,但是可能需要过个几天才行。这周我会比较忙,可能不太再会去碰这个控件了。等控件成熟之后,我会详细分析一下这个控件的实现方式。

点击这里下载源代码。

 


PS:

这里向大家道个歉。本周的WebCast,原计划是“全面讲解UpdatePanel的使用方式”,会涉及到从服务器端使用到客户端生命周期的方方面面。但是目前看来这个内容太多了。因此我会将其拆分成两次,3/29的那次只会对UpdatePanel的服务器端使用作一个完整的讲解,并且会涉及到一些UpdatePanel的实现原理。而下一次得课程,我将会对客户端的生命周期做一个全面的描述。

虽然分成了两次,但是我还是尽力保证了每次课程内容的充实性。