zoukankan      html  css  js  c++  java
  • [轉載]让UpdatePanel支持文件上传

    來源:老趙 http://www.cnblogs.com/JeffreyZhao/

    让UpdatePanel支持文件上传(1):开始

      UpdatePanel从一开始就无法支持AJAX的文件上传方式。Eilon Lipton写了一篇文章解释了这个问题的原因。文章中提供了两个绕开此问题的方法:

    1. 将“上传”按钮设为一个传统的PostBack控件而不是异步PostBack。您可以使用多种方法来这么做:例如将一个按钮放置在UpdatePanel外,将按钮设为某个UpdatePanel的PostBackTrigger,或者调用ScriptManager.RegisterPostBackControl来注册它。
    2. 建立一个不使用ASP.NET AJAX的上传页面,很多站点已经这么做了。

      不过,我们为什么不使UpdatePanel兼容FileUpload控件(<input type="file" />)呢?如果可以这样,一定能够受需要使用UpdatePanel上传文件的用户欢迎。

      我们首先要解决的问题是,找到一种能够将信息发送到服务器端的方法。我们都知道XMLHttpRequest只能发送字符串。在这里,我们使用和其他的异步上传文件的解决方案一样,使用iframe来上传文件。iframe元素是一个非常有用的东西,即使在AJAX这个概念出现之前,它已经被用于制作一些异步更新的效果了。

      其次,我们应该如何改变当前传输数据的行为呢?幸亏Microsoft AJAX Library有着强大的异步通讯层,我们可以方便创建一个UpdatePanelIFrameExetender来继承Sys.Net.WebRequestExecutor,并且将它交给一个上传文件的WebRequest对象。因此,下面的代码可以作为我们开发组件的第一步:

    第一步
    Type.registerNamespace("Jeffz.Web");
    // the new executor will use the element witch initiated the async postback.
    Jeffz.Web.UpdatePanelIFrameExecutor = function(sourceElement)
    {
    // ...
    }
    Jeffz.Web.UpdatePanelIFrameExecutor.prototype =
    {
    // ...
    }
    Jeffz.Web.UpdatePanelIFrameExecutor.registerClass(
    "Jeffz.Web.UpdatePanelExecutor", Sys.Net.WebRequestExecutor);
    Jeffz.Web.UpdatePanelIFrameExecutor._beginRequestHandler = function(sender, e)
    {
    var inputList = document.getElementsByTagName("input");
    for (var i = 0; i < inputList.length; i++)
    {
    var type = inputList[i].type;
    if (type && type.toUpperCase() == "FILE")
    {
    e.get_request().set_executor(
    new Jeffz.Web.UpdatePanelExecutor(e.get_postBackElement()));
    return;
    }
    }
    }
    Sys.Application.add_init(function()
    {
    Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(
    Jeffz.Web.UpdatePanelIFrameExecutor._beginRequestHandler);
    });

      在上面的代码段中,我们在页面初始化时监听了PageRequestManager对象的beginRequest事件。当PageRequestManager触发了一个异步请求时,我们会检查页面上是否有<input type="file" />控件。如果存在的话,则创建一个新的UpdatePanelIFrameExecutor实例,并分配给即将执行的WebRequest对象。

      根据异步通讯层的实现,WebRequest的作用只是一个保存请求信息的容器,至于如何向服务器端发送信息则完全是Executor的事情了。事实上Executor完全可以不理会WebRequest携带的信息自行处理,而我们的UpdatePanelIFrameExecutor就是这样的玩意儿。它会改变页面上的内容,将信息Post到IFrame元素中,并且处理从服务器端获得的数据。(未完待续)

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

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

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

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

    PageRequestManager.OnInit
    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属性关闭:

    关闭AJAX上传支持
    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正确收到服务器端获得的信息。这可以说是整个项目中最有技巧的部分了,我将会使用一个部分来单独讲一下这部分的机制。


    让UpdatePanel支持文件上传(3):客户端组件


       我们继续编写客户端的部分。

      我们的UpdatePanelIFrameExecutor继承了WebRequestExecutor,因此需要实现许多方法和属性。但是我们事实上不用完整地实现所有的成员,因为客户端的异步刷信机制只会访问其中的一部分。以下是异步刷信过程中会使用的成员列表,我们必须正确地实现它们:

    • get_started: 表示一个Executor是否已经开始 了。
    • get_responseAvailable: 表示一个请求是否成功。
    • get_timedOut: 表示一个请求是否超时。
    • get_aborted: 表示一个请求是否被取消了。
    • get_responseData: 获得文本形式的Response Body。 
    • get_statusCode: 获得Response的状态代码
    • executeRequest: 执行一个请求。
    • abort: 停止正在运行的请求。

      UploadPanelIFrameExecutor依旧非常简单,只是定义了一些私有变量:

    UpdatePanelIFrameExecutor构造函数
    Jeffz.Web.UpdatePanelIFrameExecutor = function(sourceElement)
    {
    Jeffz.Web.UpdatePanelIFrameExecutor.initializeBase(this);
    // for properties
    this._started = false;
    this._responseAvailable = false;
    this._timedOut = false;
    this._aborted = false;
    this._responseData = null;
    this._statusCode = null;
    // the element initiated the async postback
    this._sourceElement = sourceElement;
    // the form in the page.
    this._form = Sys.WebForms.PageRequestManager.getInstance()._form;
    // the handler to execute when the page in iframe loaded.
    this._iframeLoadCompleteHandler = Function.createDelegate(
    this, this._iframeLoadComplete);
    }


      当executeRequest方法被调用时,我们会准备一个隐藏的iframe和所有的附加的隐藏输入元素,并将form的target指向iframe。当然,其他一些工作也是必须的,例如准备一个衡量超时的计时器:

    executeRequest方法
    executeRequest : function()
    {
    // create an hidden iframe
    this._iframe = this._createIFrame();
    // all the additional hidden input elements
    this._addAdditionalHiddenElements();
    // point the form's target to the iframe
    this._form.target = this._iframe.id;
    this._form.encType = "multipart/form-data";
    // set up the timeout counter.
    var timeout = this._webRequest.get_timeout();
    if (timeout > 0)
    {
    this._timer = window.setTimeout(
    Function.createDelegate(this, this._onTimeout), timeout);
    }
    this._started = true;
    // restore the status of the element after submitting the form
    setTimeout(Function.createDelegate(this, this._restoreElements), 0);
    // sumbit the form
    this._form.submit();
    },


      建立一个隐藏得iframe元素很简单,但是我们该创建哪些附加的隐藏输入元素呢?自然我们表示“异步回送”的自定义标记是其中之一,那么剩下的还需要哪些呢?似乎我们只能通过阅读PageRequestManager的代码来找到问题的答案。还好,似乎阅读下面的代码并不困难:

    _onFormSubmit方法
    function Sys$WebForms$PageRequestManager$_onFormSubmit(evt)
    {
    // ...
    // Construct the form body
    var formBody = new Sys.StringBuilder();
    formBody.append(this._scriptManagerID + '=' + this._postBackSettings.panelID + '&');
    var count = form.elements.length;
    for (var i = 0; i < count; i++)
    {
    // ...
    // Traverse the input elements to construct the form body
    // ...
    }
    if (if._additionalInput)
    {
    formBody.append(this._additionalInput);
    this._additionalInput = null;
    }
    var request = new Sys.Net.WebRequest();
    // ...
    // prepare the web request object
    // ...
    var handler = this._get_eventHandlerList().getHandler("initializeRequest");
    if (handler)    {
    var eventArgs = new Sys.WebForms.InitializeRequestEventArgs(
    request, this._postBackSettings.sourceElement);
    handler(this, eventArgs);
    continueSubmit = !eventArgs.get_cancel();
    }
    // ...
    this._request = request;
    request.invoke();
    // ...
    }


      请注意红色部分的代码。可以发现有两种数据需要被添加为隐藏的输入元素。其一是ScriptManager相关的信息(第一部分的红色代码),其二则是变量“_additionalInput”的内容。我们很容易得到前者的值,但是后者的内容究竟是什么呢?我们继续阅读代码:

    _onFormElementClick方法
    function Sys$WebForms$PageRequestManager$_onFormElementClick(evt)
    {
    var element = evt.target;
    if (element.disabled) {
    return;
    }
    // Check if the element that was clicked on should cause an async postback
    this._postBackSettings = this._getPostBackSettings(element, element.name);
    if (element.name)
    {
    if (element.tagName === 'INPUT')
    {
    var type = element.type;
    if (type === 'submit')
    {
    this._additionalInput =
    element.name + '=' + encodeURIComponent(element.value);
    }
    else if (type === 'image')
    {
    var x = evt.offsetX;
    var y = evt.offsetY;
    this._additionalInput =
    element.name + '.x=' + x + '&' + element.name + '.y=' + y;
    }
    }
    else if ((element.tagName === 'BUTTON') &&
    (element.name.length !== 0) && (element.type === 'submit'))
    {
    this._additionalInput = element.name + '=' + encodeURIComponent(element.value);
    }
    }
    }


      _onFormElmentClick方法会在用户点击form中特定元素时执行。方法会提供变量“_additionalInput”的内容,然后紧接着,我们之前分析过的_onFormSubmit方法会被调用。现在我们就能够轻松地为form添加额外的隐藏输入元素了:

    _addAdditionalHiddenElements方法
    _addAdditionalHiddenElements : function()
    {
    var prm = Sys.WebForms.PageRequestManager.getInstance();
    // clear the array of hidden input elements
    this._hiddens = [];
    // custom sign to indicate an async postback
    this._addHiddenElement("__AjaxFileUploading__", "__IsInAjaxFileUploading__");
    // the value related to the ScriptManager
    this._addHiddenElement(prm._scriptManagerID, prm._postBackSettings.panelID);
    // find the additional data
    var additionalInput = null;
    var element = this._sourceElement;
    if (element.name)
    {
    var requestBody = this.get_webRequest().get_body();
    if (element.tagName === 'INPUT')
    {
    var type = element.type;
    if (type === 'submit')
    {
    var index = requestBody.lastIndexOf("&" + element.name + "=");
    additionalInput = requestBody.substring(index + 1);
    }
    else if (type === 'image')
    {
    var index = requestBody.lastIndexOf("&" + element.name + ".x=");
    additionalInput = requestBody.substring(index + 1);
    }
    }
    else if ((element.tagName === 'BUTTON') &&
    (element.name.length !== 0) && (element.type === 'submit'))
    {
    var index = requestBody.lastIndexOf("&" + element.name + "=");
    additionalInput = requestBody.substring(index + 1);
    }
    }
    // parse the additional data
    if (additionalInput)
    {
    var inputArray = additionalInput.split("&");
    for (var i = 0; i < inputArray.length; i++)
    {
    var nameValue = inputArray[i].split("=");
    this._addHiddenElement(nameValue[0], decodeURIComponent(nameValue[1]));
    }
    }
    },
    _addHiddenElement : function(name, value)
    {
    var hidden = document.createElement("input");
    hidden.name = name;
    hidden.value = value;
    hidden.type = "hidden";
    this._form.appendChild(hidden);
    Array.add(this._hiddens, hidden);
    },


      除去附加的隐藏输入元素非常简单,不值一提。另外iframe在加载结束后的逻辑也很容易理解——不过解析内容的机制就另当别论了:

    _iframeLoadComplete方法
    _iframeLoadComplete : function()
    {
    var iframe = this._iframe;
    delete this._iframe;
    var responseText = null;
    try
    {
    // ...
    // retrieve the data we need
    // ...
    this._statusCode = 200;
    this._responseAvailable = true;
    }
    catch (e)
    {
    this._statusCode = 500;
    this._responseAvailable = false;
    }
    $removeHandler(iframe, "load", this._iframeLoadCompleteHandler);
    iframe.parentNode.removeChild(iframe);
    this._clearTimer();
    this.get_webRequest().completed(Sys.EventArgs.Empty);
    },

    让UpdatePanel支持文件上传(4):数据传输与解析机制

      现在就要开始整个项目中最有技巧的部分了。如果我们的组件需要在多种浏览器中正常的运行,我们必须好好考虑一下发送和解析数据的方式。如果我们把这部分的机制完全交给ASP.NET AJAX原有的行为来执行,则会遇到问题。下面的代码片断就是IE 7和FireFox在收到服务器端的数据之后,iframe中的DOM结构:

    DOM结构
    <html><head></head><body><pre>33|updatePanel|ctl00_Main_UpdatePanel1|...</pre></body></html>


      很显然,这段代码的意图是为了在页面中直接显示服务器端发送过来的数据。在这种情况下,我们就可以通过“<pre />”元素的innertText属性(IE 7)或者textContent属性(FireFox)来直接获得这段文字。不幸的是,IE6的行为非常奇怪,与前两者可谓大相径庭。IE 6会把这段文字按照XML来解析,接着很自然的显示出错误信息,告诉我们这段文本不是一个有效的XML文档。这非常不合理,因为Response的“Content-Type”是“text/plain”而不是“text/xml”。这是我们要兼容多个浏览器时最头疼的情况。

      还记得我们在向客户段输出真实的数据前后都调用了WriteScriptBlock方法吗?下面就是这个方法的实现:

    WriteScriptBlock方法实现
    internal static class AjaxFileUploadUtility
    {
    internal static void WriteScriptBlock(HttpResponse response, bool begin)
    {
    string scriptBegin =
    "<script type='text/javascript' language='javascript'>window.__f__=function(){/*";
    string scriptEnd = "*/}</script>";
    response.Write(begin ? scriptBegin : scriptEnd);
    }
    }


      IE 6和IE 7会将使用<script />来包含的文本作为一段脚本代码来处理。我们这里在真实的数据两边加上了脚本定义的内容,使它成为了客户端iframe中“__f__”方法的一段注释,因此我们可以通过调用这个方法的toString函数来获得这个方法的文本内容。请注意在RenderPageCallback方法中,我们把文本进行了编码,将“*/”替换为“*//*”,然后再将其发送到客户端,这么做的目的是使这段文本能够成为合法的JavaScirpt代码。

    RenderPageCallback
    StringBuilder sb = new StringBuilder();
    HtmlTextWriter innerWriter = new HtmlTextWriter(new StringWriter(sb));
    renderPageCallbackMethodInfo.Invoke(
    this.PageRequestManager,
    new object[] { innerWriter, pageControl });
    writer.Write(sb.Replace("*/", "*//*").ToString());


      等一下,我们在这里把异步刷新运行正常时输出的文本进行了编码,但是我们在异常情况下的输出并没有这么做,不是吗?没错。因为在异常状况下,错误信息会通过Response的Write方法直接输出(请看PageRequestManager类的OnPageError方法),因此我们无法向前面的代码那样获得它输出的结果。我们现在只能希望错误信息中不要出现“*/”这样的字符串吧(当然,我们可以使用反射机制来重写整个逻辑,但是这样做实在比较复杂)。

      下面,我们就要在客户端的_iframeLoadComplete方法中重新获取这段文本了:

    _iframeLoadComplete与_parseScriptText方法实现
    _iframeLoadComplete : function()
    {
    //...
    try
    {
    var f = iframe.contentWindow.__f__;
    var responseData = f ? this._parseScriptText(f.toString()) :
    this._parsePreNode(iframe.contentWindow.document.body.firstChild);
    if (responseData.indexOf("\r\n") < 0 && responseData.indexOf("\n") > 0)
    {
    responseData = responseData.replace(/\n/g, "\r\n");
    }
    this._responseData = responseData;
    this._statusCode = 200;
    this._responseAvailable = true;
    }
    catch (e)
    {
    this._statusCode = 500;
    this._responseAvailable = false;
    }
    // ...
    },
    _parseScriptText : function(scriptText)
    {
    var indexBegin = scriptText.indexOf("/*") + 2;
    var indexEnd = scriptText.lastIndexOf("*/");
    var encodedText = scriptText.substring(indexBegin, indexEnd);
    return encodedText.replace(/\*\/\/\*/g, "*/");
    },


      我们在这里将判断iframe的window对象中是否存在“__f__”方法,而不是直接判断浏览器的类型来决定下面要做的事情,因为这样可以带来更多的浏览器兼容性。

      FireFox的行为则完全不是这样的,它依旧使用我们一开始提到的那种DOM结构,把从服务器端得到的文本显示在iframe中,这种做法比较合理,因为Response的Content-Type为“text-plain”。因此,我们会使用另一种方法来得到这段文本:

    _parsePreNode方法实现
    _parsePreNode : function(preNode)
    {
    if (preNode.tagName.toUpperCase() !== "PRE") throw new Error();
    return this._parseScriptText(preNode.textContent || preNode.innerText);
    },


      请注意,“_iframeLoadComplete”方法中还有几行非常重要的代码:

    _iframeLoadComplete方法中非常重要的代码
    if (responseData.indexOf("\r\n") < 0 && responseData.indexOf("\n") > 0)
    {
    responseData = responseData.replace(/\n/g, "\r\n");
    }


      由于从服务器端得到的脚本将会被分割为多个部分,每个部分的格式为“length|type|id|content”,因此字符串的长度是在解析文本时非常重要的属性。因此,我们将会把所有的“\r”替换成“\r\n”,以此保持内容和长度的一致,否则解析过程将会失败。而且事实上,这样的替换只会出现在FireFox中。(未完待续)

     
    让UpdatePanel支持文件上传(5):支持页面重定向的HttpModule

      我们现在试用一下这个组件。

      首先,我们将AjaxUploadHelper控件放置在页面中,紧跟在ScriptManager之后,因为AjaxUploadHelpe需要在第一时间告诉ScriptManager目前正处在一个异步刷新的过程中。

    使用AjaxFileUploadHelper控件
    <%@ Register Assembly="AjaxFileUploadHelper" Namespace="Jeffz.Web" TagPrefix="jeffz" %>
    //...
    <asp:ScriptManager ID="ScriptManager1" runat="server" />
    <jeffz:AjaxFileUploadHelper runat="server" ID="AjaxFileUploadHelper1" />
    //...


      接着,在页面上添加一个UpdatePanel,并在其中放置一个FileUpload控件,一个按钮以及一个Label。为了更容易地看出异步刷新的效果,我们在页面上添加两个时间:

    Page
    <%= DateTime.Now %>
    <asp:UpdatePanel ID="UpdatePanel1" runat="server">
    <ContentTemplate>
    <%= DateTime.Now %><br />
    <asp:FileUpload ID="FileUpload1" runat="server" />
    <asp:Button ID="Button1" runat="server" Text="Upload" OnClick="Button1_Click" /><br />
    <asp:Label ID="Label1" runat="server" Text=""></asp:Label>
    </ContentTemplate>
    </asp:UpdatePanel>


      在Code Behind代码中,我们为Button添加Event handler:

    Code Behind
    protected void Button1_Click(object sender, EventArgs e)
    {
    if (this.FileUpload1.PostedFile != null)
    {
    this.Label1.Text = this.FileUpload1.PostedFile.ContentLength + " bytes";
    }
    else
    {
    this.Label1.Text = "";
    }
    }


      打开页面,我们可以看到页面中显示了那些控件和两个时间。

    1

      选择一个文件并点击Upload按钮,我们可以发现只有UpdatePanel内部的时间被改变了,文件大小也显示在了页面上:

    2


      很震撼吧?但是如果我们改变Code Behind中的代码:

    改变Code Behind中的代码
    protected void Button1_Click(object sender, EventArgs e)
    {
    this.Response.Redirect("AnotherPage.aspx", true);
    }


      刷新页面,点击按钮,您就会发现……失败了?为什么?

      原因如下:在一个“普通”的PostBack时,如果我们在执行了Redirect方法,浏览器将会接受到一个Status Code为302的Response,以及一个跳转目标。接着浏览器就会将用户带去指定的目标页面。当XHR发出的请求得到这样一个Response之后,它将会自动重新请求而不会告诉客户端究竟发生了什么。这时,客户端只能获得目标跳转之后的资源,而并非起初请求的资源。

      因此,ASP.NET AJAX提供了一个组件来支持异步PostBack时的跳转。这个组件就是ScriptModule,我们可以在web.config文件中找到它的注册信息。

    web.config文件的信息
    <system.web>
    <!-- other configurations -->
    <httpModules>
    <add name="ScriptModule"
    type="System.Web.Handlers.ScriptModule, System.Web.Extensions, ..."/>
    </httpModules>
    <!-- other configurations -->
    </system.web>
    <!-- for IIS 7 -->
    <system.webServer>
    <!-- other configurations -->
    <modules>
    <add name="ScriptModule" preCondition="integratedMode"
    type="System.Web.Handlers.ScriptModule, System.Web.Extensions, ..."/>
    </modules>
    <!-- other configurations -->
    </system.webServer>


      下面的代码片断就是它解决这个问题的实现:

    ScriptModule
    public class ScriptModule : IHttpModule
    {
    protected virtual void Init(HttpApplication context)
    {
    context.PreSendRequestHeaders += new EventHandler(PreSendRequestHeadersHandler);
    // ...
    }
    private void PreSendRequestHeadersHandler(object sender, EventArgs args)
    {
    HttpApplication application = (HttpApplication)sender;
    HttpResponse response = application.Response;
    if (response.StatusCode == 302)
    {
    if (PageRequestManager.IsAsyncPostBackRequest(application.Request.Headers))
    {
    string redirectLocation = response.RedirectLocation;
    List<HttpCookie> cookies = new List<HttpCookie>(response.Cookies.Count);
    for (int i = 0; i < response.Cookies.Count; i++) {
    cookies.Add(response.Cookies[i]);
    }
    response.ClearContent();
    response.ClearHeaders();
    for (int i = 0; i < cookies.Count; i++)
    {
    response.AppendCookie(cookies[i]);
    }
    response.Cache.SetCacheability(HttpCacheability.NoCache);
    response.ContentType = "text/plain";
    PageRequestManager.EncodeString(response.Output, "pageRedirect",
    String.Empty, redirectLocation);
    }
    else if //...
    }
    }
    }


      我们响应了PreSendRequestHeaders事件,它将会在服务器端发送Header信息之前被触发。此时,如果Status Code为302(表示Response将要使客户端跳转到另一个页面去),则会清除所有即将发送的内容,并重新指定传输的信息。在这里最重要的修改就是Response Body的内容。因为客户端将要解析收到的字符串,因此我们必须发送格式为“length|type|id|content”。请注意上方红色的代码,它将会发送一段格式合法的字符串,例如“16|pageRedirect||/AnotherPage.aspx|”。

      在客户端,我们可以找到下面的实现,它的作用是在收到页面重定向的信息之后跳转页面。请注意下方红色的代码:

    客户端支持页面重定向的代码
    function Sys$WebForms$PageRequestManager$_onFormSubmitCompleted(sender, eventArgs)
    {
    // ...
    for (var i = 0; i < delta.length; i++) {
    var deltaNode = delta[i];
    switch (deltaNode.type) {
    case "updatePanel":
    Array.add(updatePanelNodes, deltaNode);
    break;
    // ...
    case "pageRedirect":
    window.location.href = deltaNode.content;
    return;
    //...
    }
    }
    // ...
    }


      明白了这点之后,我们也就能够轻松地编写一个这样的模块了:

    AjaxFileUploadModule
    public class AjaxFileUploadModule : IHttpModule
    {
    public void Init(HttpApplication context)
    {
    context.PreSendRequestHeaders += new EventHandler(PreSendRequestHeadersHandler);
    }
    private void PreSendRequestHeadersHandler(object sender, EventArgs e)
    {
    HttpApplication application = (HttpApplication)sender;
    HttpResponse response = application.Response;
    if (response.StatusCode == 302 &&
    AjaxFileUploadUtility.IsInIFrameAsyncPostBack(application.Request.Params))
    {
    string redirectLocation = response.RedirectLocation;
    List<HttpCookie> cookies = new List<HttpCookie>(response.Cookies.Count);
    for (int i = 0; i < response.Cookies.Count; i++)
    {
    cookies.Add(response.Cookies[i]);
    }
    response.ClearContent();
    response.ClearHeaders();
    for (int i = 0; i < cookies.Count; i++)
    {
    response.AppendCookie(cookies[i]);
    }
    response.Cache.SetCacheability(HttpCacheability.NoCache);
    response.ContentType = "text/plain";
    AjaxFileUploadUtility.WriteScriptBlock(response, true);
    StringBuilder sb = new StringBuilder();
    TextWriter writer = new StringWriter(sb);
    AjaxFileUploadUtility.EncodeString(writer, "pageRedirect",
    String.Empty, redirectLocation);
    response.Write(sb.Replace("*/", "*//*").ToString());
    AjaxFileUploadUtility.WriteScriptBlock(response, false);
    response.End();
    }
    }
    public void Dispose() {}
    }


      上方红色的代码为我们的Module与ASP.NET AJAX中的ScriptModule之间唯一的区别。我们在web.config文件中注册了AjaxFileUploadModule之后,我们在服务器端调用Redirect方法之后,在客户端就能进行跳转了。此时客户端接收到的文本如下:

    客户端收到的文本
    <script type='text/javascript' language='javascript'>window.__f__=function()
    {/*16|pageRedirect||/AnotherPage.aspx|*/}</script>

    点击这里下载整个项目

    English Version

  • 相关阅读:
    蒙特卡罗算法之素数测试
    蒙特卡罗算法之主元素问题
    拉斯维加斯随机化算法求解整数因子分解
    拉斯维加斯算法之n后问题
    舍伍德算法之跳跃表问题
    舍伍德算法之线性时间选择问题
    X oracle 11g 旧的归档日志无法清理
    问题 C: B C++时间类的运算符重载
    设有三个进程A、B、C,其中A与B构成一对生产者与消费者(A为生产者,B为消费者),共享一个由n个缓冲块组成的缓冲池;B与C也构成一对生产者与消费者(此时B为生产者,C为消费者)共享另一个由m个缓冲块组成的缓冲池。用P、V操作描述它们之间的同步关系。
    某寺庙,有小和尚、老和尚若干。有一水缸,由小和尚用水桶从井中提水入缸,老和尚用水桶从缸里取水饮用。水缸可容10桶水,水取自同一井中。水井径窄,每次只能容一个水桶取水。水桶总数为3个。每次入、取缸水仅为1桶,且不可以同时进行。试用P、V操作给出小和尚、老和尚动作的算法描述。
  • 原文地址:https://www.cnblogs.com/godwar/p/976651.html
Copyright © 2011-2022 走看看