不论好坏,UpdatePanel 控件都是 ASP.NET AJAX 社区所喜爱的。我说“好”,是因为 UpdatePanel 使部分页面呈现变得相当简单,而说“坏”,是因为它的简便和易用性是以效率和令人啼笑皆非的带宽为代价的。
UpdatePanel 可以为一般的网页带来 AJAX 神奇的好处,但是它不能提供我们与 AJAX 正常关联的高效性。例如,您是否知道,当 UpdatePanel 控件对服务器执行异步 AJAX 回调以更新其内容时,这个请求包含了常规 ASP.NET 回发所包含的一切,其中还包括视图状态呢?大多数开发人员会以为 AJAX 取消了视图状态。而 UpdatePanel 的 AJAX 品牌却并非如此。
如果您准备使用 UpdatePanel 控件,您需要清楚您在准备干什么。在许多情况下,从性能的角度而言,应用程序最好是不使用 UpdatePanel,而是使用对 WebMethods 或页面方法的异步调用。这样做可能会大幅度降低网络传输中的数据量。但是,它也是一个根本转变,在这里 UI 更新需要由开发人员使用该页面上的 JavaScript 来进行显式处理。
此外,ASP.NET AJAX 论坛上到处都有关于自定义 UpdatePanel 的提问。其实,只要您了解为 UpdatePanel 提供客户端支持的 Microsoft® AJAX Library 中的 PageRequestManager、JavaScript 类,这里面许多问题就可以迎刃而解。
既然已经提供了 ASP.NET AJAX,我想进一步检查一下 UpdatePanel,从而进一步了解您可以如何对其进行自定义和优化,甚至在没有它的情况下如何运行。这恰恰也是本期专栏包含的全部内容。
更新突出显示
有时候您无法帮助 Microsoft 的开发人员,而只能对他们表示遗憾。如果他们不能将工作做得足够好,就会遭到公众的抨击。然而,有时候他们的工作做得很出色,也会遭到抨击。例如,我最近收到了一位客户的电子邮件,抱怨 ASP.NET AJAX UpdatePanel 运行得有点过头了。
UpdatePanel 使 ASP.NET 页面回发到服务器,并将它变为流畅、无闪烁的更新时出现闪光和闪烁变得极其简单。UpdatePanel 可以通过将回发转换成异步回调(XML-HTTP 请求),以及使用客户端上的 JavaScript 以刷新由 UpdatePanel 控件封装的页面的一部分来发挥它的魔力。闪光和闪烁会消失,因为浏览器不会将该页面重新绘制成与它在回发期间的一样。
客户的抱怨是,用户有时候不会注意到页面的那个部分已经更新了新内容。他的问题很简单:ASP.NET AJAX 团队的工作人员是否能使 UpdatePanel 闪烁再多一点,以便用户不会错过重要的更新?
不幸的是,ASP.NET AJAX 团队可能对制造 UpdatePanel 闪烁没什么兴趣。毕竟,消除闪烁才是发明 UpdatePanel 的初衷。但值得高兴的是,您可以在浏览器中使用 AJAX 的一些神奇功能,以吸引对已更新的 UpdatePanel 的注意。秘诀在于 Microsoft AJAX Library(由客户端一半的 ASP.NET AJAX 组成的 JavaScript 类的库)中的 Sys.WebForms.PageRequestManager 类。PageRequestManager 可以管理由 UpdatePanel 启动的异步回调。它还负责在异步回调完成后更新 UpdatePanel 内的内容。
PageRequestManager 可以在更新前和更新后激发浏览器中的事件。您可以将 JavaScript 中的这些事件关联起来,并运行可以提醒用户注意更新内容的代码。关键事件被命名为 pageLoaded。此事件每次都会激发浏览器中的页面加载(它类似 Page_Load in ASP.NET)。它每次还会激发代表 UpdatePanel 控件完成而启动的异步回调,并且会更新这个 UpdatePanel 中的内容。您可以使用两行代码(可以合并为一行)注册 pageLoaded 事件的 JavaScript 处理程序:
var prm = Sys.WebForms.PageRequestManager.getInstance(); prm.add_pageLoaded(pageLoaded);
第一行获得对该页面的 PageRequestManager 对象的引用。第二行注册名为 pageLoaded 的 JavaScript 函数,作为 pageLoaded 事件的处理程序。
调用时,pageLoaded 事件处理程序会收到一个 Sys.WebForms.PageLoadedEventArgs 类型的参数,它是 Microsoft AJAX Library 中的另一个类。PageLoadedEventArgs 包含一个 get_panelsUpdated 方法,您可以调用该方法来枚举所有的 UpdatePanel(如果有),其内容刚刚已更新。在默认情况下,UpdatePanel 就是客户端上的 DIV,因此您可以使用 JavaScript 来使该 DIV 闪光,突出显示它,或对它执行任何您想要的操作,以提醒用户注意它。
图 1 列出的代码显示了一个使用 pageLoaded 事件来执行更新突出显示的方法。每次更新时,这个 JavaScript 都会使表示已更新的 UpdatePanel 的文档对象模型 (DOM) 元素闪光,方法是使它们依次快速显示和隐藏三次。闪光是通过名为 flashPanels 的帮助器函数来执行的,它将闪光次数作为输入参数。
请注意,已更新的 UpdatePanel 的可见性可以通过切换打开和关闭来创建闪光效果。除了与 DOM 元素直接交互以外,代码还会封装代表具有 Sys.UI.Control 对象的 UpdatePanel 的 DOM 元素。然后,它使用 Sys.UI.Control 的 set_visible 和 get_visible 方法来切换可见性:
_panels[i].set_visible(!_panels[i].get_visible());
Sys.UI.Control 是在 Microsoft AJAX Library 中,具体而言是在 MicrosoftAjax.js 中找到的 JavaScript 类。以这种方式切换可见性的好处是,这是独立于浏览器的。这项操作在支持 ASP.NET AJAX 的每种浏览器(几乎就是所有的现代浏览器)中同样有效。另一方面,与浏览器 DOM 直接交互的 JavaScript 代码必须予以调整,以便在不同的浏览器类型中使用。
取消 UpdatePanel 更新
pageLoaded 事件是 UpdatePanel 返回服务器更新其内容时,PageRequestManager 类激发的若干事件之一。PageRequestManager 激发的另一个重要事件是 initializeRequest,它在发生异步回调之前激发。
最近有人问我,是否有可能在运行时决定是否允许 AsyncPostBackTrigger 触发一个 UpdatePanel 更新。回答是肯定的。这个操作通过处理 initializeRequest 事件来完成。
传递到 initializeRequest 处理程序的第二个参数是 initializeRequestEventArgs 类型的一个对象。这个对象包含 get_postBackElement 方法,它可以识别触发更新的按钮或其他元素。它还有一个您可用来在回调发生之前将其取消的 set_cancel 方法。下面是使用中的 set_cancel 方法的示例:
<script type=”text/javascript”> var prm = Sys.WebForms.PageRequestManager.getInstance(); prm.add_initializeRequest(initializeRequest); function initializeRequest(sender, args) { args.set_cancel(!confirm(‘Are you sure?’)); } </script>
在这个示例中,intializeRequest 处理程序可以在回调执行之前弹出一个确认框,询问用户是否要继续更新。单击确认框中的“Cancel”(取消)可以将 true 传递给 set_cancel,这样可以停止执行回调。在现实生活中,您可能觉得没必要在允许继续更新之前提示用户确认,但如果要是能够根据应用程序中其他地方的条件取消更新,它就可能很有用。
顺便说一下,它还有可能在异步回调执行后但又尚未完成前取消它们。PageRequestManager 提供了 abortPostBack 方法来执行此操作;它还提供了 get_isInAsyncPostBack 方法来确定异步回调是否挂起。这些方法通常与 UpdateProgress 控件一起使用,以便显示取消 UI。
多个 UpdatePanel
一个页面可以承载几个 UpdatePanel。默认情况下,当一个页面上的 UpdatePanel 更新时,该页面的其他 UpdatePanel 也会更新。有时候这是您想要的,但多半您并不需要每个 UpdatePanel 更新来响应其他 UpdatePanel。
通过将页面上每个 UpdatePanel 控件的 UpdateMode 属性设置为 Conditional,您可以选择更新哪个 UpdatePanel 实例(及更新的时间)。然后,在 UpdatePanel 更新和调用服务端事件处理程序的时候,请调用您要更新的其他面板上的 UpdatePanel.Update。这样可以通过减少呈现的控件的数量来减轻服务器的负载,而且它还减少了响应中的数据量,因为不进行更新的 UpdatePanel 不会将任何数据添加到响应中。
不使用 UpdatePanel 更新
AJAX 不仅可以创建更好的用户体验,它还可以提供更高效的网络通信。当发生传统的 ASP.NET 回发时,Web 窗体中包括视图状态在内的所有数据,都会传输到回发中的服务器。视图状态是 ASP.NET 页面,尤其是使用 DataGrid 和 GridView 控件的 ASP.NET 页面似乎会反应迟缓的一个原因。具有太多视图状态的页面会降低性能,并且具有太多视图状态的页面在 ASP.NET 应用程序中都太常见。
用 AJAX 回调替代 ASP.NET 回发的好处之一是能正确完成,AJAX 回调仅传送需要被传送的数据。这意味着,它们不必将视图状态包括在该传输中。
当您使用 UpdatePanel 在一个页面上执行无闪烁更新时,您可能会认为您在进行高效构建。毕竟,UpdatePanel 使用的是 AJAX,不是吗?不幸的是,如果您在 UpdatePanel 更新时检验一下网络中的通信,您会发现您根本就没有保存什么东西,至少是在发送的时候没有保存。通常在回发期间传送到服务器的视图状态数据(与其他数据)也会在 UpdatePanel 回调期间传送。事实上,来自 UpdatePanel 的异步 XML-HTTP 请求中所增长的数据几乎与在标准 ASP .NET 回发中增长的数据相同。下面是有关 ASP.NET AJAX 不可告人的秘密:UpdatePanel 虽易于使用,但是通信效率不高。
几乎没有什么办法可让您提高 UpdatePanel 的效率,但是您可以放弃使用 UpdatePanel,并转而使用 ASP.NET AJAX 的其他功能来更新页面内容,它不仅同样流畅,而且更加高效。它只需要多一点点力气,但是最后的结果往往让人觉得是值得付出的,因为您可以大大降低在客户端与服务器之间传输的数据量。
您还可以减少服务器上的负载。当 UpdatePanel 回调到服务器时,被回调定为目标的页面会完成几乎整个生命周期 — 该页面会被实例化,该页面中的控件也会被实例化,并且 UpdatePanel 内的控件也会完成一个正常呈现循环。那是更新该网页一部分的大笔开销。
作为示例,请考虑图 2 中的页面段。它提供了一个允许用户键入邮政编码的简单 UI,并且单击按钮就可以用城市和州来初始化城市和州字段(请参见图 3)。所有的控件都承载在 UpdatePanel 中,因此 Button 控件的回发被转换为异步回调,并且事件处理程序 (GetCityAndState) 会被调用到该回调内部的服务器上。GetCityAndState(未显示代码)从邮政编码文本框中读取邮政编码,将其转换成城市和州,并相应地初始化表示城市和州的 TextBox 和 DropDownList。由于这些都发生在 UpdatePanel 内部,所以更新非常顺畅,并且无闪烁。
现在有一个问题。UpdatePanel 已使用这种方法改善了用户的体验,但是它并不能减少在网络中传输的数据量。UpdatePanel 几乎也不能减轻服务器上的负载,到 UpdatePanel 内部的控件呈现为止,在服务器上执行的处理几乎与在完备回发期间所发生的一样。它必须是这种方法,因为 UpdatePanel 控件的好处之一是服务器端的事件处理程序(如 GetCityAndState)在异步回调内执行的操作与在传统回发中所执行的操作一样。这意味着该页面上的控件必须被实例化,它们必须有权访问视图状态等。
图 4 说明了如何在不使用 UpdatePanel 控件的情况下实现相同功能。这次,Autofill 按钮被绑定到一个 JavaScript,这个 JavaScript 激发了异步 XML-HTTP 请求给名为 GetCityAndState 的 ASMX Web 方法。该调用是通过名为 ZipCodeService 的 JavaScript 代理置入的,它由 ScriptManager 控件中的服务引用生成。GetCityAndState 以 ZIP Code 字符串作为输入,并返回一个包含相应的城市和州两个项的字符串数组。完成函数 onGetCityAndStateCompleted 会检索该数组中的项,并将它们插入到城市和州字段。从外部看起来结果是一样,但是它在内部的运行方式却是迥异。
以下说明了如何通过 JavaScript 代理实现调用 ASMX Web 方法:
[ScriptService] public class ZipCodeService : System.Web.Services.WebService { [WebMethod] public string[] GetCityAndState(string zip) { ... } }
除了它所属的类的属性是 ScriptService,而非 WebService 这一点以外,这是一个从各方面来看都可称为标准的 Web 方法。虽然 ScriptService 与 WebService 具有相同的意义,但是它还包含了附加的意义,那就是 Web 服务的 WebMethods 可以从客户端脚本调用。
除了允许常规 WebMethods 充当 XML-HTTP 请求的目标之外,ASP.NET AJAX 还支持一种特殊类别的 Web 方法,称为页面方法。页面方法是在 Web 页面中实现的 WebMethods,也就是在 ASPX 文件或 codebehind 文件中,而不是在 ASMX 文件中。Page 方法允许开发人员在不构建专业 Web 服务的情况下提供用于 XML-HTTP 回调的端点。
页面方法必须是公开的静态方法,并且和 WebMethods 一样,必须具有 WebMethod 属性。(WebMethods 和页面方法还可以具有 ScriptMethod 属性,该属性可提供退出网络的附加控件。)在客户端,页面方法可以通过特殊的 PageMethods 代理从 JavaScript 调用。
与 Web 服务不同,页面方法无需服务引用。但是,您必须通过将 ScriptManager 控件的 EnablePageMethods 属性设置为 true 来启用页面方法,如下所示:
<asp:ScriptManager ID=”ScriptManager1” runat=”server” EnablePageMethods=”true” />
实质上,页面方法提供了与 WebMethods 一样的高效性。当调用页面方法时,视图状态和其他输入未被传送到服务器。并且既然页面方法都是静态的,那么就可以在没有实例化页面对象的情况下调用它们。对页面方法的调用不会调用由常规 ASP.NET 请求触发的页面生命周期。
Web Service != SOAP 和 XML
ASP.NET AJAX 最重要的功能之一是可以调用使用浏览器客户端的异步 XML-HTTP 请求的服务器上的 WebMethods 和页面方法。但是当我告诉别人这个功能的时候,我不禁感觉有点畏缩。
我们大多数人听到“Web 服务”这个术语时,都会想到 SOAP 和 XML。为了语言简练,通常不会在同一个句中同时提到这两种技术。是的,您可以使用 ASP.NET AJAX 从 JavaScript 中调用 WebMethods。但是您错了,ASP.NET AJAX 并不使用 SOAP 和 XML。
图 5 显示了当执行图 4 中的 Web 方法调用时在网络中传输的数据。除了 HTTP 标头外,请求中被传输的唯一数据是用户键入的 ZIP Code,而响应中返回的唯一数据是一对表示城市和州的字符串。您不会看到任何 SOAP 或 XML(或视图状态,就此而言)。相反,输入和输出会通过使用 JavaScript Object Notation (JSON) 进行编码,它比 XML 简短得多,也更易处理。请求和响应并不使用 SOAP,而是使用一个简单的协议,而这个协议基本上就是 HTTP。HTTP 和 JSON 的组合使对 WebMethods 和页面方法的 ASP.NET AJAX 调用比传统的 Web 服务调用要有效得多。
JSON 是一个正在崭露头角的行业标准序列化格式。它还是被 ASP.NET AJAX 使用的本机格式。Microsoft AJAX Library 的 Sys.Serialization.JavaScriptSerializer 类支持在客户端进行 JSON 序列化和反序列化。System.Web.Script.Serialization.JavaScriptSerializer 类支持在服务器上进行 JSON 序列化和反序列化。
并非所有类型均与 JSON 兼容。例如,JSON 不能处理具有循环引用的对象。当您需要返回不能与 JSON 兼容的复杂数据类型时,其实您可以使用 ASP.NET AJAX 的 ScriptMethod 属性将返回类型序列化为 XML。这个技术对返回 XML 数据的方法也很有用,如下所示:
[ScriptMethod (ResponseFormat=ResponseFormat.Xml)] public XmlDocument GetData() { ... }
此外,您还可以构建和注册自定义 JSON 转换器,它允许将通常不能与 JSON 兼容的类型序列化和反序列化。ASP.NET AJAX January Futures CTP 包含三个这样的转换器:一个针对 DataSet,一个针对 DataTable,还有一个针对 DataRow。