代码改变世界

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

2007-04-11 15:02 by Jeffrey Zhao, ... 阅读, ... 评论, 收藏, 编辑

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

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代码。

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 : 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 : function(preNode)
{
    if (preNode.tagName.toUpperCase() !== "PRE") throw new Error();
    return this._parseScriptText(preNode.textContent || preNode.innerText);
},

请注意,“_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中。(未完待续)

 

点击这里下载整个项目

English Version

使用Live Messenger联系我