带局部渲染和事件的 ASP.NET Web Form 模型






4.97/5 (17投票s)
本文以局部渲染和 AJAX 异步回发为参考,解释了基于事件的 ASP.NET Web Form 编程模型在 Web 应用程序中的应用。
引言
要了解 Web 应用程序环境及其与 ASP.NET AJAX 相关的约束,请阅读我的上一篇文章:关于 ASP.NET AJAX 的 Web 应用程序注意事项。
正如该文章中讨论的,我们知道 Web 应用程序是无状态的,它以断开连接模式工作,即 Web 服务器总是将传入的客户端请求视为新的,与之前的请求没有任何连续性。在理解基于事件的 ASP.NET Web Form 编程模型中的基本概念时,请记住这一点。
事件驱动编程模型本身是从著名的好莱坞原则中概念化的:“不要打电话给我们,我们会打电话给你”(好莱坞原则)。这是一个非常有用的范例,它有助于我们开发易于理解(高内聚)和易于维护(低耦合)的代码。这是如何实现的呢?实现这一点的关键是牺牲传统程序流中的控制元素。因此,在我们的案例中,不是应用程序驱动系统(包括 ASP.NET 运行时),而是 ASP.NET 运行时驱动我们的应用程序。
为了使这种编排(使用基于事件的编程模型)成功并与 ASP.NET 框架无缝集成,我们作为开发人员,因此必须编写遵循框架(在本例中为 ASP.NET)的一些约定和要求的代码,这就是我们将在本文中讨论的内容。
我不会涵盖严谨的细节,例如文档对象模型 (DOM) 和 DOM 中的事件传播(事件冒泡)等。但我会尝试说明 ASP.NET 设计人员想要实现什么以及我们如何通过使用此模型获得好处。本文着眼于服务器端和客户端事件模型,然后使用 ASP.NET 服务器控件,我们将从客户端到服务器再返回(回发中的往返)进行演练。
我使用 Fiddler 工具来拦截通过线路传输的数据。我们将分析这些数据,以深入了解页面每次往返中客户端和服务器之间流动的数据负载。请注意,我们不会深入探讨 ASP.NET 的内部工作原理,描述项目中的每个发布文件,而是从概念层面讨论一切。
概述
在 ASP.NET Web Form 模型中,一个页面由两部分组成:可视元素(HTML、服务器控件、静态文本、CSS、JavaScript 等)和页面的编程逻辑。Microsoft 的 IDE (Visual Studio) 将这两个不同的部分存储在两个单独的文件中。可视元素在 *.aspx* 文件中创建,代码在单独的类文件(称为代码隐藏类文件,*.aspx.vb* 或 *.aspx.cs*)中。当然,正如您将看到和讨论的,将 CSS 样式和 JavaScript 代码用于浏览器内的页面行为,而不是 *.aspx* 文件中,以获得更好的可读性和可重用性,这是一种更好的实践。您将在随附的示例中找到这种方法。
正如上面所讨论的,尽管 Web Form 页面由两个独立的文件组成,但当我们的应用程序运行时,它们共同形成一个单元。项目中所有 Web Form 的代码隐藏类文件被编译成项目生成的动态链接库(*.dll*)文件。Web Form 的 *.aspx* 页面文件也经过编译,但以不同的方式进行。每次请求 Web Form 页面时,服务器都会运行 *.dll* 文件。在运行时,此 *.dll* 文件处理传入请求并通过创建动态输出并将其发送回浏览器或客户端设备来响应。
如果页面包含服务器控件(如示例所示),则派生页面类充当控件的容器。控件的实例在服务器运行时创建,然后 `Page` 类为浏览器或客户端设备渲染输出。
这个 `Page` 类实际上建模了整个 Web Form 页面。页面经历一系列处理阶段(称为页面生命周期)。这些阶段包括初始化、实例化控件、恢复和维护状态、执行事件处理程序代码,最后是渲染。服务器控件(在开发自定义服务器控件时需要了解这一点)也遵循类似的页面生命周期,它们包含在页面中。
`Page` 类有一个独特的“渲染”阶段,它发生在页面生命周期的末尾,此时会生成输出。请注意,“渲染”是一个处理阶段,而不是一个事件。在渲染之前,页面的 View State(页面 View State 由 ASP.NET 以隐藏的 `` 控件的形式管理,用于在页面往返服务器时持久化状态/信息)和所有控件的 View State 都已保存。在渲染阶段,页面为每个控件调用 `Render` 方法,提供一个文本写入器来写入其输出。
重要的是要注意,每次调用页面时,`Page` 类都会执行这些步骤。每次发生与服务器的往返时,页面都会被初始化、处理和处置。
在页面生命周期的每个阶段,页面都会引发事件,我们可以处理这些事件以自定义和运行我们自己的代码。对于控件事件,我们通过声明式地使用“`onclick`”等属性,或在代码中将事件处理程序绑定到事件。
下图说明了 *.aspx*、*.aspx.cs* 或 *.aspx.vb* 与 ASP.NET 运行时类之间的关系。例如,一个名为 `_Default` 的新类是从 `System.Web.UI.Page` 派生出来的。
public partial class _Default : Page
*.aspx* 页面文件反过来继承自派生的 `_Default` 类。
由于当用户浏览页面时,*.aspx* 文件是动态编译的,因此它与类文件之间的关系是通过页面顶部的 `script` 指令建立的。在 ASP.NET Web Form 中,`@Page` 指令(或用户控件文件中的 `@Control`)包含指定 *.aspx* 文件与其代码隐藏文件之间关系的属性。例如:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs"
Inherits="_Default" EnableEventValidation="false" %>
服务器每次收到请求时都会加载一个 ASP.NET 页面,并在请求完成后将其卸载。页面及其包含的服务器控件负责执行请求并将 HTML 渲染回客户端。
尽管客户端和服务器之间的通信是无状态和断开连接的,但此 ASP.NET Web Form 模型的主要目标是客户端必须体验到类似于在其自己的桌面上连续执行的有状态进程。此模型的另一个目标是帮助像我们这样的开发人员在像 Microsoft Visual Studio 这样的快速应用程序开发 IDE 中快速构建 Web 应用程序。
有状态连续性的幻觉是由 ASP.NET 页面框架以及页面及其控件创建的。在回发时,控件必须表现得好像它从上一个 Web 请求结束时停止的地方开始。
Microsoft 的 ASP.NET 页面框架设计人员使状态管理相对容易实现,但像我们这样的控件和页面开发人员必须了解控件执行序列才能实现连续性效果。这是我们需要从 ASP.NET 页面生命周期中了解的作为事件序列,当 Web Form 页面由 Web 服务器上的 ASP.NET 运行时处理时。
ASP.NET 中的事件驱动编程
首先,让我们回顾一下背景知识。事件模型在 Visual Basic 和 Visual C++ 使用 MFC 的 Windows 桌面应用程序开发中非常流行。这与基于顺序过程的模型(由流程图表示)大相径庭。在事件模型中,每个应用程序状态都是一个对象,事件作为触发器将一个应用程序状态转换为另一个状态。状态作为一个对象将我们的关注点从控制流转移到状态的属性。流被委托给一个事件(触发器),该事件使状态之间发生转换。鉴于这种方法的优点,Microsoft 扩展了此模型,并引入了服务器控件以及基于页面的 Web Form 编程模型到 ASP.NET 中。在此模型中,应用程序开发人员无需关注如何从用户界面 (GUI) 获取输入或向用户界面渲染输出(例如 HTML)的细节。相反,我们应该关注附加到 UI 组件的事件处理程序中的应用程序特定行为,这些 UI 组件接收来自 ASP.NET 运行时的事件或触发器。如前所述,传统上,事件被用作各种组件之间解耦的一种方式。例如,在 ASP.NET 中,UI 控件(例如按钮)的功能可以与另一个 Web 应用程序集成,而无需更改 UI 控件本身(通过 Visual Studio 设计器中的拖放)。
Web 应用程序是一种客户端-服务器应用程序,其中客户端与服务器通过互联网分离。为了使在服务器上运行的事件处理程序代码能够响应用户在浏览器中的 Web Form 上所做的更改,控件必须将执行从客户端浏览器转移回 Web 服务器。在 ASP.NET 中,`asp:Button` 等服务器控件负责通过在单击时生成表单回发来处理此问题。随着本文的进展,我们将很快看到所有这些细节。
事件和事件处理
事件是指当发生值得关注的事情时出现的通知。事件允许一个对象(发送者)通知其他对象(接收者)发生了特殊的事情。一个很好的例子是按钮点击。
本质上,事件处理程序类似于回调。它们之间只存在细微的差异。从这个角度来看,事件是一种匿名广播,而回调是一种握手。
当我们为 ASP.NET 按钮的 Click 事件编写事件处理程序时,我们实际上是在编写回调方法的实现。但是,我们不需要显式调用事件处理程序。相反,ASP.NET Web Forms 框架的 `Page` 类充当通知源,因为它会在恰当的时机自动执行我们的事件处理程序方法(程序执行流的控制权在于 ASP.NET 运行时)。
现在,让我们关注事件处理机制。组件不调用在编译时已知的函数,而是调用我们在运行时提供的函数(后期绑定)。引发或触发事件意味着调用处理程序。为了实现这一点,接收事件(或通知)的组件首先向事件源组件提供其事件处理程序代码的指针,这个过程称为注册。
事件与委托
为了处理事件,Microsoft 的 ASP.NET 设计者引入了一种新的类型——委托。委托通常被定义为面向对象的函数指针。委托在事件发布者和事件订阅者之间提供必要的间接层。从设计上讲,事件发布者对任何订阅者都一无所知。因此,订阅者的职责是向事件发布者注册或注销自己。因此,委托总是返回 `void`,因为事件发布者无法从事件订阅者的返回值中做任何事情。
如上所述,发布者上的事件与特定方法(即订阅者上的事件处理程序)之间的绑定是使用事件委托完成的。ASP.NET 页面框架在运行时自动连接(或连接)这些方法的相应委托实例。当我们声明一个委托时,我们实际上是定义了一个类。
当我们将事件与自定义事件处理程序声明性绑定时,编译器会完成必要的步骤。
- 声明一个委托对象,其签名与事件处理程序的方法签名完全匹配。
- 使用关键字 `event` 引用委托对象,因此定义该关键字。
- 定义事件处理方法。
- 创建一个委托对象并插入我们想要封装并注册到将触发事件的对象的方法。
// Step-1. Delegate type defining the prototype // of the callback method that receivers must implement public delegate void OnClickEventHandler (object sender, EventArgs e); // Step-2. On server control public event OnClickEventHandler Click; // Step-3. Protected, virtual method responsible // for notifying registered objects of the event protected virtual void OnSubmit (object sender, EventArgs e) { ... }
和 `OnInit`
// Step-4. Protected, virtual method responsible for notifying this.Click += new EventHandler(this.OnSubmit);
`Click` 是事件的名称。此事件的类型为 `OnClickEventHandler`,这意味着所有事件通知的接收者都必须提供一个回调方法,其原型与 `OnClickEventHandler` 委托的原型匹配。如前所述在 *.aspx* 页面中,如果我们将属性 `AutoEventWireup="true"` 设置为 true,那么我们就不必编写上面所示的必要管道代码。
示例项目介绍
有两个使用相同应用程序的示例,两者都使用 ASP.NET 服务器控件 `Button` 和 `GridView`。
- 一个没有局部渲染,即没有 `ScriptManager`/`UpdatePanel` 控件。
- 另一个有局部渲染。
这个例子是一个简单的应用程序。它允许用户从应用程序服务器上的“ImportFiles”文件夹中导入 CSV 文件列表中的内容。如果导入数据上的完整性检查一切正常,它还允许用户将导入的数据与数据库同步。这两个操作通过单击“Import”和“Synchronize”这两个按钮来启动。“Import”和“Synchronize”是两个不同的步骤,在此示例中,我们没有添加 CSV 解析、导入数据上的完整性检查以及最后数据库同步过程的详细信息;相反,我们通过延迟和页面上简单的进度动画(busy.gif)模拟了它们。在 `Page Load` 事件上,查看文件名属性,只会从“/Importfiles”文件夹中选择按时间顺序排列的最新文件名,然后将其与 Web.config 文件中 `
需要注意的是,用户可以在完成同步之前多次导入同一组文件。因此,如果新文件集与旧导入文件集匹配,则不应有提示,因为用户意图明确,即他或她想纠正一些错误。当上一个导入文件名与上一个同步文件名不匹配时,应用程序会提示用户。提示是:“上次导入的数据尚未同步并将丢失。继续?”。如果“/Importfiles”文件夹中同一类别中有多个文件,则只能导入最新未同步的文件。一旦同步,就不能重新导入。在此示例中,我们通过禁用按钮控件来做到这一点。一旦按钮被禁用,重新开始操作的唯一方法是刷新整个页面,整个过程从头开始。下图显示了示例应用程序的状态及其与事件的转换。
处理网页
以下部分说明了用户从客户端浏览器键入 ASP.NET 页面 URL 时通常发生的序列(在这个简单示例中,我们没有使用登录页面,但通常,这是任何需要身份验证的应用程序的起始页面)。
- 用户在浏览器中请求 Default.aspx 网页。
- Web 服务器 (IIS) 将此调用转发到 ASP.NET 运行时。ASP.NET 运行时随后查找页面的程序集,如果不存在,则从 *.aspx* 文件及其关联的代码隐藏文件编译页面类。ASP.NET 运行时随后执行代码,创建 HTML,然后将其发送到浏览器。我们可以使用 Fiddler 工具拦截流量并查看 HTML 代码。请注意,服务器控件被替换为纯 HTML、CSS 和 JavaScript。
- 浏览器渲染 HTML,显示示例中的简单表单,如下图所示。图 2
- 如图所示,页面分为两个独立的部分(用虚线隔开)——顶部部分包含可供导入的 .CSV 文件列表。底部部分描述了上次导入的文件列表和上次同步的文件列表信息。
- 当用户单击“导入”按钮时,浏览器会识别出“导入”按钮已被单击。表单的方法是 POST,操作在 Default.aspx.cs 中编码。因此,我们有了所谓的到原始 .aspx 文件的回发。请参阅下面 Fiddler 拦截屏幕的快照。图 3a. 显示了正常回发,而图 3b. 显示了带局部渲染的 AJAX 异步回发,如下文所述。
图 3a图 3b
- 收到回发请求后,服务器上的 ASP.NET 运行时现在执行此页面的处理,该处理经历各个阶段,并被称为前面讨论的页面生命周期。当用户单击“导入”按钮时,会引发一个事件;服务器会识别此事件,并调用 `Page` 类中的事件处理程序 `btnImport_Click`。
- 在模拟的处理延迟和进度动画之后,示例更新了底部部分的相应行,即 `LastImportAgencyFile`、`LastImportFacilityFile` 和 `LastImportLocationFile`。
- 然后服务器将整个响应发送到浏览器。请注意,3b. 的响应负载小于 3a.。
- 浏览器再次渲染页面以进行正常回发,`PageRequestManager` 在异步回发的情况下进行 DOM 更新。现在,用户看到导入已成功完成,因为底部上次导入的文件名与顶部部分的导入文件名以及当前导入日期时间匹配。
- ASP.NET 使用 Web Form 中名为 `__VIEWSTATE` 的隐藏字段在客户端保留状态。
- 当我们考虑 ASP.NET Web Form 上下文中的事件模型时,一个有趣的事情是,**事件**因用户的各种操作(在此示例中,单击“导入”按钮)而在客户端引发,并通过页面类上的委托和事件处理程序在服务器上处理。
在上面描述的典型场景中,页面在每次往返(称为回发)时都会重新创建。一旦服务器完成处理并将页面发送到浏览器,它就会丢弃页面信息。
这是保留 Web 服务器资源和使 Web 应用程序可伸缩的必要步骤。下次提交页面时,服务器会重新开始创建 `Page` 类的新实例,然后再次开始处理它。因此我们看到 ASP.NET Web 页面本质上是无状态的——页面的变量和控件的值默认情况下不会在服务器上保留。但是,为了保持连续性,ASP.NET 通过在往返之间保存控件属性来解决此限制。这在示例中通过保存到 View State 来完成,View State 是 ASP.NET 提供的众多机制之一。我们还使用了隐藏的 `` 字段作为占位符,用于多个标志,以协调客户端事件处理程序和服务器端事件处理程序。
ASP.NET 状态管理功能的示例包括:
- 视图状态
- 控制状态
- 隐藏字段
- Cookie
- 查询字符串
- 应用程序状态
- 会话状态
- 配置文件属性
视图状态、控制状态、隐藏字段、Cookie 和查询字符串都涉及以各种方式在客户端存储数据,而应用程序状态、会话状态和配置文件属性都将数据存储在服务器内存中。我们不会在这篇文章中进一步详细说明,因为这是一个不同的主题——这里有一个很好的 CodeProject 文章参考:ASP.NET 状态管理技术初学者介绍。
ASP.NET 还会检测何时首次请求表单以及何时回发表单(通过检查 `Page.IsPostBack`),这使我们能够相应地进行编程。
(注意:如果我们比较两个示例项目中的 `default.aspx` 页面,我们会发现,在“EventandAjaxExample”中,我们必须使用“`EndRequestHandler`”来隐藏异步回发的动画显示。对于“EventExample”,则没有,并且也不需要,因为完全页面刷新会自动将动画显示设置为“`none`”——“`style=display: none`”)。
服务器端页面事件
ASP.NET 允许我们在服务器代码中为从浏览器传递的事件设置事件处理程序。假设用户正在与此示例中的 Web Form 页面交互,该页面包含一个“导入”(按钮)服务器控件。用户单击“导入”,将引发一个事件,该事件通过 HTTP POST 传输到服务器,ASP.NET 页面框架在此解释发布的信息并将引发的事件与适当的事件处理程序相关联,该事件处理程序是根据我们的应用程序逻辑定制的处理程序实现。框架随后执行此定制处理程序代码:`protected void btnImport_Click(object sender, EventArgs e)`。使用客户端状态和 HTTP POST 协议,ASP.NET 服务器控件给人一种印象,即它们在客户端维护内存,并通过引发事件并在服务器上处理事件来响应用户交互。对于我们开发人员来说,我们需要覆盖 `virtual protected` 事件处理程序(在示例中给出)作为自定义的一部分,以引入我们的应用程序特定代码——其余由 ASP.NET 框架提供。
下图从概念上说明了这种机制。
ASP.NET 处理捕获、传播和解释客户端浏览器上生成的所有事件的机制。我们在 Web Form 页面中创建事件处理程序,并且我们不需要考虑这种机制,因为它在幕后发生,并且完全由 ASP.NET 运行时管理。
请注意,这些类型的 Web Form 事件需要往返或回发到服务器进行处理,因此我们应根据应用程序逻辑并着眼于性能选择性地使用它们。
对于服务器控件,某些事件,通常称为“点击事件”,会导致 Web Form 被回发到服务器。而像 `TextBox` 控件等服务器控件中的更改事件,则会被捕获,但不会立即导致 POST。相反,它们会被控件缓存起来,直到下次发生 POST。此时,当页面再次在服务器上处理时,所有待处理的事件都会被处理。
通常情况下是这样,但当情况需要立即 POST 时,支持更改事件的服务器控件会包含一个 `AutoPostBack` 属性。当此属性设置为 `true` 时,控件的更改事件会导致表单立即 POST,而无需等待单击事件。例如,默认情况下,`DropDownList` 控件的 `OnSelectedIndexChanged` 事件不会导致页面提交。但是,通过将控件的“`AutoPostBack`”属性设置为 `true`,我们指定一旦用户从列表中选择一个项目,页面就会发送到服务器以处理事件(这样,我们可以显示某个城市的邮政编码)。
事件参数
ASP.NET 中的事件传递两个参数:一个表示引发事件的对象(`object sender`),以及一个包含任何事件特定信息(`EventArgs e`)的第二个对象。第二个参数通常是 `System.EventArgs` 类型,但对于某些控件,它是该控件特有的类型。例如,对于 `ImageButton` 服务器控件,第二个参数是 `ImageClickEventArgs` 类型,其中包含用户单击坐标的信息。同样,对于 `TreeView` 控件的按需 `PopulateNode` 事件处理程序,第二个参数是 `TreeNodeEventArgs` 类型。
ASP.NET 页面生命周期
前面,我们简要介绍了 ASP.NET 页面生命周期作为服务器上不同的处理阶段,其中大部分是 ASP.NET 事件的最终结果;现在,让我们看看它们发生的顺序。
由于随附的第二个示例是使用 ASP.NET 局部渲染的,因此我包含了下面的 ASP.NET 页面生命周期图,其中包括 AJAX 回发和局部渲染。对于带和不带局部渲染的回发,`PreRenderComplete` 之后的阶段是不同的,如下所述。
局部渲染
让我们更详细地了解 ASP.NET 中 AJAX 回发与局部渲染时发生的情况。要使 ASP.NET 页面成为局部渲染页面,我们必须首先向页面添加 `asp:ScriptManager`,然后通过用 `asp:UpdatePanel` 控件包装它们来定义可独立更新的区域。当启用局部页面更新时,控件可以异步发布到服务器。异步回发的行为类似于常规回发,因为生成的服务器页面执行完整的页面和控件生命周期。但是,对于异步回发,页面更新仅限于包含在 `asp:UpdatePanel` 控件中并标记为要更新的页面区域。服务器只将受影响元素的 HTML 标记发送到浏览器。在浏览器中,客户端 `Sys.WebForms.PageRequestManager` 类执行文档对象模型 (DOM) 操作,用更新后的标记替换现有 HTML。这是示例中的一部分。
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server"
EnablePartialRendering="true" AsyncPostBackTimeout="100"/>
<script type="text/javascript" language="javascript">
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(EndRequestHandler);
</script>
<div class ="Container">
<asp:Panel ID="pnlImport" runat="server" Width="100%" CssClass="Page">
<div class="Container">
<asp:UpdatePanel runat="server" ID="UpdatePanelDataImportFiles"
UpdateMode="Conditional">
<ContentTemplate>
<h1 class="Header">
<asp:Label runat="server" ID="lblHeading"
Text="<%$Resources:EventResources, lblCsvDataImport %>"></asp:Label>
</h1>
<p class="Label">
<asp:Label ID="lblFiles" runat="server" Text=""></asp:Label>
</p>
<asp:GridView ID="gvFiles" runat="server" AutoGenerateColumns="False"
CssClass="Gridview" Width="100%">
<Columns>
<asp:BoundField
HeaderText="<%$Resources:EventResources, gvFileName %>"
DataField="FileName"/>
</Columns>
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>
</div>
`asp:UpdatePanel` 控件只是在原始标记周围添加了一个 `
无论何时脚本管理器在页面中检测到一个或多个 `asp:UpdatePanel` 控件,它都会发出一个脚本块,如下所示(您可以使用 Fiddler 工具进行验证)。
//<![CDATA[ Sys.WebForms.PageRequestManager._initialize('ScriptManager1',
// document.getElementById('form1'));
Sys.WebForms.PageRequestManager.getInstance()._updateControls(
['tUpdatePanelDataImportFiles','tUpdatePanelSyncInfo'], [], [], 100); //]]>
`_initialize` 方法是客户端 `Sys.WebForms.PageRequestManager` 对象上的一个静态方法(参见 MicrosoftAjaxWebForms.js)。它创建 `PageRequestManager` 类的一个全局实例并对其进行初始化。该类充当单例,单个实例以后可以通过 `getInstance` 方法检索,就像在第二个语句中那样。上面的第二个语句还向客户端框架注册了一个 `UpdatePanel` 控件数组。在我们的示例中,有两个 `UpdatePanel`([`tUpdatePanelDataImportFiles`, `tUpdatePanelSyncInfo`])。每个服务器端 `UpdatePanel` 控件都通过其 ID 引用。
这里发生的关键操作在 `_initialize` 方法内部。如前所述,在创建类的单例实例后,代码会对其进行初始化。此时,除其他事项外,还会为 DOM 表单对象的提交事件注册一个处理程序。这意味着每当页面提交表单时,AJAX 脚本都会启动并使用 `XMLHttpRequest` 发出请求,而不是让请求通过正常的浏览器回发。保留了原始的表单字段集,并为服务器端脚本管理器的方便附加了一些额外信息。因此,AJAX 回发上传的信息比常规 ASP.NET 回发略多。
ViewState 以及任何其他隐藏字段都会随请求一起被携带并上传到服务器。在返回的路上,更新后的 ViewState 会与新的隐藏字段(如果有)以及可能更短的标记一起下载——在 Fiddler 截图中,参考上面的图 3b,它被突出显示为 5,791 字节。特别是,响应仅包含在回发期间已修改的可更新区域的标记。该列表包括触发回发的 `UpdatePanel` 控件 `UpdatePanelSyncInfo`。尽管在示例中没有,但这将包括任何嵌套面板、页面中 `UpdateMode` 属性设置为“`Always`”的任何其他 `UpdatePanel` 控件,以及任何通过服务器端自定义编码进行程序化刷新的 `UpdatePanel` 控件。
AJAX 回发的响应是一个文本流,如上图 3b 右下侧所示。请注意,在上图 3b 中,当页面中包含基于 AJAX 的 `ScriptManager` 和 `UpdatePanel` 控件时,清除浏览器缓存并首次加载页面时,会加载额外的 WebResource.axd 和 ScriptResource.axd。
对于使用 ASP.NET AJAX 的局部渲染,我们需要 `UpdatePanel` 控件以及 `ScriptManager` 控件。如果页面中包含 `ScriptManager` 控件并且 `UpdatePanel` 包含任何控件,则 `UpdatePanel` 中的控件可以通过 AJAX 异步更新。
深入了解 ASP.NET AJAX
要跟踪通过 Internet Explorer® 的所有 Web 流量,我们将使用 Fiddler HTTP 调试器代理应用程序,您可以从其网站下载。为此,请浏览本文中给出的 AJAX 示例,并观察通过 Fiddler 发生的情况;我们可以看到,每次我们调用部分回发事件时,都会发生实际的 HTTP POST。HTTP POST 请求——图 3b,右上方部分,从上往下第五行,包含常见的 HTTP 头,但它也包含一个我们从未见过的头。
x-microsoftajax: Delta=true
这个头是关键。一旦 `ScriptManager` 控件在服务器上识别出这个头,它就不会被动地将引用注入页面输出,而是会检查表单 POST 中发送的数据,并以客户端脚本能够理解的格式将响应渲染回客户端。
除了将服务和脚本引用写入初始页面输出外,`ScriptManager` 控件还将初始化客户端功能。客户端 `PageRequestManager` 负责跟踪由 `ScriptManager` 注册的控件生成的所有事件。
当客户端触发回发事件时,`PageRequestManager` 将识别它是否由为局部渲染而识别的任何控件引起。如果是,`PageRequestManager` 将取消回发事件并重新打包。然后,来自回发事件的重新打包数据将使用客户端类 `Sys.Net.WebRequest` 传输到服务器。POST 请求中会设置 `x-microsoftajax: Delta=true` 头,并通过 `Sys.Net.WebRequest` 发送到服务器。
在服务器端,`ScriptManager` 将通过 `LoadPostData` 接收到表单 POST 中的数据通知。`LoadPostData` 允许单个控件筛选表单 POST 以获取相关信息。(这是一个标准事件,并非 ASP.NET AJAX 特有)。如果参考上面图 5 中的页面生命周期事件,我们会看到 `LoadPostData` 发生在页面 `InitComplete` 之后。在 `LoadPostData` 中,`ScriptManager` 控件识别导致表单 POST 的控件以及该控件所在的 `UpdatePanel`。
`ScriptManager` 控件现在已经识别出部分回发,并识别出导致回发的控件。`ScriptManager` 控件完全覆盖了其宿主页面的默认 `Render` 方法。现在我们看到我们的新 `Page` 类引入了它自己的格式来将控件渲染到页面输出流中。
最后,客户端框架从服务器获取异步响应并解析数据。`ScriptManager` 控件已将所有控件 ID 和新标记打包到响应中,以便客户端框架可以在浏览器的文档对象模型 (DOM) 上解析脚本操作以更新页面内容。鉴于整个过程是异步发生的,浏览器屏幕会快速无声地更新,网页用户获得了更好的体验,没有任何交互暂停或闪烁。
结合客户端和服务器事件
我们无法在 Web 服务器控件的 HTML 语法中指定客户端事件,例如针对“导入”按钮服务器控件。相反,在服务器代码运行时使用以下代码向控件添加事件属性:
btnImport.Attributes.Add("onclick", "confirmImport('" +
Resources.EventResources.alertImport + "');");
结论
在本文中,我们探讨了 ASP.NET Web Form 编程模型中的概念,以及 ASP.NET AJAX 回发中的局部渲染。随后,我们分析了在往返或回发期间客户端和服务器之间传输的有效负载。在基于 ASP.NET Web Form 模型的示例应用程序的代码演练中,我们试图理解这种方法中的驱动力。微软设计人员以一种抽象的开发模型,使其完全符合 Windows 桌面开发模型,并有助于快速应用程序开发(RAD)。正如您所看到的,我们需要理解这种编程模型的设计意图,以便将其应用于我们的 Web 应用程序。
最后,关于 ASP.NET Web Form 模型在局部渲染方面的局限性。例如,在这种模型中,如果同时发生两次局部渲染调用,旧的调用会被终止,为新的调用腾出空间。如果您需要寻找更智能的 AJAX 启用 ASP.NET Web Form 模型实现,请参阅:Gaia Ajax:Dino Esposito 撰写的 ASP.NET AJAX 新方法。
最后,请注意,本文并非建议 ASP.NET Web Form 编程模型是开发 Web 应用程序的最佳方式,尤其是在我们考虑可测试性和关注点分离时,但它在某些场景下是足够的,并且有助于快速程序开发。
参考文献
- 好莱坞原则
- 委托介绍,作者 Jeffrey Richter
- 委托 第 2 部分,作者 Jeffrey Richter
- 使用委托实现事件,作者 Jeffrey Richter
- 用户控件的全面考察,作者 Scott Mitchell
- 比较 Web Forms 和 ASP.NET MVC,作者 Dino Esposito
- 架构演示模式比较,作者 Shivprasad Koirala
- 实用的 ASP.NET:文章集,作者 Peter Vogel 及其他人
- 微软的 Web 演示模式
- 模型-视图-演示者 (用于在您的 Web 应用程序中创建可测试的 UI),作者 Jean-Paul Boodhoo
- Gaia Ajax:ASP.NET AJAX 的新方法,作者 Dino Esposito
致谢
这个特定主题实际上是我与两位暑期学生 Balu Avhad 先生和 Mayur Potulwar 先生(浦那大学)进行动手研究的成果。他们帮助我通过 Fiddler 拦截客户端和 Web 服务器之间的页面流量,分析有效负载数据。
历史
- 初稿于 2010 年 4 月 13 日提交。
- 于 2010 年 4 月 15 日添加了图 1a。