Hello World
Spiga

让UpdatePanel支持文件上传(2):服务器端组件

2007-04-05 11:55 by 老赵, 6725 visits

我们现在来关注服务器端的组件。目前的主要问题是,我们如何让页面(事实上是ScriptManager控件)认为它接收到的是一个异步的回送?ScriptManager控件会在HTTP请求的Header中查找特定的项,但是我们在向IFrame中POST数据时无法修改Header。所以我们必须使用一个方法来“欺骗”ScriptManager。

目前使用的解决方案是,我们在POST数据之前在页面中隐藏的输入元素(<input type="hidden" />)中放入一个特定的标记,然后我们开发的服务器端组件(我把它叫做AjaxFileUplaodHelper)会在它的Init阶段(OnInit方法)中在Request Body中检查这个标记,然后使用反射来告诉ScriptManager目前的请求为一个异步请求。

但是事情并不像我们想象的那么简单,让我们在写代码之前来看一个方法:

internal sealed class PageRequestManager
{
    // ...

    internal void OnInit()
    {
        if (_owner.EnablePartialRendering && !_owner._supportsPartialRenderingSetByUser)
        {
            IHttpBrowserCapabilities browser = _owner.IPage.Request.Browser;
            bool supportsPartialRendering =
                (browser.W3CDomVersion >= MinimumW3CDomVersion) &&
                (browser.EcmaScriptVersion >= MinimumEcmaScriptVersion) &&
                browser.SupportsCallback;

            if (supportsPartialRendering)
            {
                supportsPartialRendering = !EnableLegacyRendering;
            }
            _owner.SupportsPartialRendering = supportsPartialRendering;
        }

        if (_owner.IsInAsyncPostBack)
        {
            _owner.IPage.Error += OnPageError;
        }
    }

    // ...
}

上面这段代码会在ScriptManager的OnInit方法中被调用。请注意红色部分的代码,“_owner”变量是当前页面上的ScriptManager。在页面受到一个真正的异步会送之后,PageRequestManager会响应页面的Error事件,并且将错误信息用它定义的格式输出。如果我们只是修改了ScriptManager的私有field,那么如果在异步回送时出现了一个未捕获的异常,那么页面就会输出客户端未知的内容,导致在客户端解析失败。所以我们必须保证这种情况下的输出和真正的异步回送是相同的,所以我们就可以使用以下的做法来解决错误处理的问题。

internal static class AjaxFileUploadUtility
{
    internal static bool IsInIFrameAsyncPostBack(NameValueCollection requestBody)
    { 
        string[] values = requestBody.GetValues("__AjaxFileUploading__");

        if (values == null) return false;

        foreach (string value in values)
        {
            if (value == "__IsInAjaxFileUploading__")
            {
                return true;
            }
        }

        return false;
    }

    // ...
}

[PersistChildren(false)]
[ParseChildren(true)]
[NonVisualControl]
public class AjaxFileUploadHelper : Control
{
    // ScriptManager members;
    private static FieldInfo isInAsyncPostBackFieldInfo;
    private static PropertyInfo pageRequestManagerPropertyInfo;

    // PageRequestManager members;
    private static MethodInfo onPageErrorMethodInfo;
    private static MethodInfo renderPageCallbackMethodInfo;


    static AjaxFileUploadHelper()
    {
        Type scriptManagerType = typeof(ScriptManager);
        isInAsyncPostBackFieldInfo = scriptManagerType.GetField(
            "_isInAsyncPostBack",
            BindingFlags.Instance | BindingFlags.NonPublic);
        pageRequestManagerPropertyInfo = scriptManagerType.GetProperty(
            "PageRequestManager",
            BindingFlags.Instance | BindingFlags.NonPublic);

        Assembly assembly = scriptManagerType.Assembly;
        Type pageRequestManagerType = assembly.GetType("System.Web.UI.PageRequestManager");
        onPageErrorMethodInfo = pageRequestManagerType.GetMethod(
            "OnPageError", BindingFlags.Instance | BindingFlags.NonPublic);
        renderPageCallbackMethodInfo = pageRequestManagerType.GetMethod(
            "RenderPageCallback", BindingFlags.Instance | BindingFlags.NonPublic);
    }

    public static AjaxFileUploadHelper GetCurrent(Page page)
    {
        return page.Items[typeof(AjaxFileUploadHelper)] as AjaxFileUploadHelper;
    }

    private bool isInAjaxUploading = false;

    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);

        if (this.Page.Items.Contains(typeof(AjaxFileUploadHelper)))
        {
            throw new InvalidOperationException("One AjaxFileUploadHelper per page.");
        }

        this.Page.Items[typeof(AjaxFileUploadHelper)] = this;

        this.EnsureIsInAjaxFileUploading();
    }

    private void EnsureIsInAjaxFileUploading()
    {
        this.isInAjaxUploading = 
AjaxFileUploadUtility.IsInIFrameAsyncPostBack(this.Page.Request.Params); if (this.isInAjaxUploading) { isInAsyncPostBackFieldInfo.SetValue( ScriptManager.GetCurrent(this.Page), true); this.Page.Error += new EventHandler(Page_Error); } } private void Page_Error(object sender, EventArgs e) { // ... } private object _PageRequestManager; private object PageRequestManager { get { if (this._PageRequestManager == null) { this._PageRequestManager = pageRequestManagerPropertyInfo.GetValue( ScriptManager.GetCurrent(this.Page), null); } return this._PageRequestManager; } } // ... }

这段实现并不复杂。如果Request Body中的“__AjaxFileUploading__”的值为“__IsInAjaxFileUploading__”,我们就会使用反射修改ScirptManager控件中的私有变量“_isInAsyncPostBack”。此后,我们使用了自己定义的Page_Error方法来监听页面的Error事件,当页面的Error事件被触发时,我们定义的新方法就会将能够正确解析的内容发送给客户端端。

自然,AjaxFileUploadHelper也需要将程序集中内嵌的脚本文件注册到页面中。我为组件添加了一个开关,可以让用户开发人员使用编程的方式来打开/关闭对于AJAX文件上传的支持。这部分实现更为简单:

public bool SupportAjaxUpload
{
    get { return _SupportAjaxUpload; }
    set { _SupportAjaxUpload = value; }
}

protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);

    if (this.isInAjaxUploading)
    {
        this.Page.SetRenderMethodDelegate(new RenderMethod(this.RenderPageCallback));
    }

    if (this.Page.IsPostBack || !this.SupportAjaxUpload) return;

    if (!ScriptManager.GetCurrent(this.Page).IsInAsyncPostBack)
    {
        ScriptReference script = new ScriptReference(
            "Jeffz.Web.AjaxFileUploadHelper.js", this.GetType().Assembly.FullName);
        ScriptManager.GetCurrent(this.Page).Scripts.Add(script);
    }
}

如果用户希望关闭对于AJAX文件上传的支持,他可以使用下面的代码将页面上AjaxFileUploadHelper控件的SupportAjaxUpload属性关闭:

AjaxFileUploadHelper.GetCurrent(this.Page).SupportAjaxUpload = false;

等一下,这是什么?我是指在“OnPreRender”方法中的代码:

if (this.isInAjaxUploading)
{
    this.Page.SetRenderMethodDelegate(new RenderMethod(this.RenderPageCallback));
}

解释如下:在ScirptManager的“OnPreRender”方法执行时,页面的Render方法会被服务器端PageRequestManager类的RenderPageCallback方法替代。上面代码的作用是在“我们的”异步回送时,再次使用我们定义的方法来替换页面的Render方法。请注意之前的Page_Error方法也是我们重新定义的方法,当异步回送时遇到了未捕获的异常时会使用它来输出,请注意下面的代码:

private void RenderPageCallback(HtmlTextWriter writer, Control pageControl)
{
    AjaxFileUploadUtility.WriteScriptBlock(this.Page.Response, true);

    StringBuilder sb = new StringBuilder();
    HtmlTextWriter innerWriter = new HtmlTextWriter(new StringWriter(sb));
    renderPageCallbackMethodInfo.Invoke(
this.PageRequestManager, new object[] { innerWriter, pageControl }); writer.Write(sb.Replace("*/", "*//*").ToString()); AjaxFileUploadUtility.WriteScriptBlock(this.Page.Response, false); } private void Page_Error(object sender, EventArgs e) { AjaxFileUploadUtility.WriteScriptBlock(this.Page.Response, true); onPageErrorMethodInfo.Invoke(this.PageRequestManager, new object[] { sender, e }); AjaxFileUploadUtility.WriteScriptBlock(this.Page.Response, false); }

究竟什么是“AjaxFileUploadUtility.WriteScriptBlock”方法呢?我们为什么要这样写?其实这么做的目的是为了兼容各种浏览器,使它们都能够正确通过iframe正确收到服务器端获得的信息。这可以说是整个项目中最有技巧的部分了,我将会使用一个部分来单独讲一下这部分的机制。

 

点击这里下载整个项目

English Version

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

13 条回复

  1. Artech
    *.*.*.*
    链接

    Artech 2007-04-05 12:33:00

    坐个沙发,支持一下(老赵的沙发很难坐呀,呵呵)!

  2. 非我
    *.*.*.*
    链接

    非我 2007-04-05 12:39:00

    挤着坐坐 ^_^

  3. help[未注册用户]
    *.*.*.*
    链接

    help[未注册用户] 2007-04-05 19:02:00

    楼主可以 提供一个 DEMO 吗, 谢谢

  4. 老赵
    admin
    链接

    老赵 2007-04-05 19:13:00

    @help
    您把AjaxFileUploadHelper紧贴着放在ScriptManager之后,然后在web.config中添加AjaxFileUploadModule这个HttpModule就可以了。:)

  5. MK2
    *.*.*.*
    链接

    MK2 2007-04-05 19:43:00

    忍不住了,看英文版的第三篇````了

  6. MK2
    *.*.*.*
    链接

    MK2 2007-04-05 20:07:00

    WebSite使用之前你发的那篇
    但要改Page中的
    <%@ Register Assembly="AjaxFileUploadHelper" Namespace="Jeffz.Web" TagPrefix="jeffz" %>

    添加HttpModule:
    <httpModules>
    <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
    <add name="AjaxFileUploadModule" type="Jeffz.Web.AjaxFileUploadModule" />
    </httpModules>


    测试成功。。。。。

  7. 老赵
    admin
    链接

    老赵 2007-04-05 21:46:00

    @MK2
    呵呵。:)

  8. 老赵
    admin
    链接

    老赵 2007-04-06 04:25:00

    已经下载过源代码的朋友们可以再下载一次,以前的版本漏了一句代码,在Release模式下会出现问题。
    // 就是initializeBase这句代码。

  9. fengmk2[未注册用户]
    *.*.*.*
    链接

    fengmk2[未注册用户] 2007-04-08 00:58:00

    // restore the status of the element after submitting the form
    setTimeout(Function.createDelegate(this, this._restoreElements), 0);

    为什么不直接调用呢? 有什么奥秘?

  10. 老赵
    admin
    链接

    老赵 2007-04-08 15:12:00

    @fengmk2
    因为需要先submit再取出这些,直接调用的话,就白做那么多事情了(改变form属性,添加hidden elements),用setTimeout则可以在方法结束之后调用。

  11. lkm888[未注册用户]
    *.*.*.*
    链接

    lkm888[未注册用户] 2007-08-08 15:48:00

    使用时总是把控件放入页面时显示:Jeffz.Web.AjaxFileUpLoadHelper 的类型初始值设定项引发异常。

    请赐教!

  12. 老赵
    admin
    链接

    老赵 2007-08-08 17:00:00

    @lkm888
    对比一下我的例子和您的例子呢?

  13. 学习·[未注册用户]
    *.*.*.*
    链接

    学习·[未注册用户] 2009-07-18 23:51:00

    在模板中无法使用··的问题?

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我