惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
A
About on SuperTechFans
博客园 - Franky
Engineering at Meta
Engineering at Meta
Recent Announcements
Recent Announcements
云风的 BLOG
云风的 BLOG
B
Blog
Microsoft Security Blog
Microsoft Security Blog
L
LangChain Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
U
Unit 42
Martin Fowler
Martin Fowler
Y
Y Combinator Blog
Stack Overflow Blog
Stack Overflow Blog
博客园 - 叶小钗
Vercel News
Vercel News
Apple Machine Learning Research
Apple Machine Learning Research
The Cloudflare Blog
Last Week in AI
Last Week in AI
腾讯CDC
Microsoft Azure Blog
Microsoft Azure Blog
爱范儿
爱范儿
V
V2EX
G
Google Developers Blog

博客园 - nacarat

使用ASP.NET AJAX的注意事项 使用ASP.NET AJAX实现幻灯片效果 提高网页打开速度的一些小技巧 ASP.NET AJAX(开发代号Atlas)重要参考资源大收集 如何生成静态页面的五种方案 ASP.NET验证控件祥解 全面了解 Cookie的传递流程、编程实现及安全问题 Asp.Net中虚拟文件系统的使用 如何彻底的做到代码和界面分离 用ASP.NET将原始图片按照指定大小按原始比例缩放显示图片 SQL注入详解 ASP.NET中如何防范SQL注入式攻击 学习ASP.NET比较完整的流程 如何在客户端获取上传图片的大小 观点:.NET发展中的几个失误 asp.net控件开发基础(4) JS代码的格式化和压缩 asp.net控件开发基础(3) asp.net控件开发(2)
让UpdatePanel支持文件上传
nacarat · 2008-10-01 · via 博客园 - nacarat

让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);
},

  现在就要开始整个项目中最有技巧的部分了。如果我们的组件需要在多种浏览器中正常的运行,我们必须好好考虑一下发送和解析数据的方式。如果我们把这部分的机制完全交给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中。(未完待续)

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

  首先,我们将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>

点击这里下载整个项目