ASP.NET DaST 应对 MVC 和 WebForms 的挑战





5.00/5 (1投票)
DaST 是一种新的架构模式,用于构建高度动态的 Web 2.0 应用程序。网页被渲染成一组随机嵌套的矩形,每个矩形单独控制,并且可以通过 AJAX 部分更新任何矩形组合。
不久前,我提出了 DaST 作为一种新的架构模式,用于构建高度动态的 Web 2.0 应用程序。简而言之,网页被渲染成一组随机嵌套的矩形,每个矩形单独控制,并且可以通过 AJAX 部分更新任何矩形组合。该概念目前已作为开源 ASP.NET DaST 框架在 ASP.NET 中实现。我最近发布了该框架的第一个稳定版本并完成了文档。本文是 DaST 教程,总结了项目启动以来所做的工作。
DaST 技术是 Web 开发范式的一次重大转变。它专门设计用于将创建高度动态的 Web 2.0 应用程序变成一项简单的任务。ASP.NET DaST 证明了小巧的开源工具集可以完成最高复杂性的工作,在简单性、架构、灵活性、性能、演示分离以及许多其他重要的 Web 开发方面都超越了标准 WebForms 和 MVC 等庞然大物。我知道这听起来怎么样,但我可以保证,在本文的后续内容中,DaST 方法将彻底改变你对 Web 应用程序设计和开发的看法。
简要概述
第一印象很重要。我把这个 1 分钟的概述放在这里,在正文之前,是为了突出 DaST 的一些功能,以便你对这项技术以及如何利用它在你的 Web 应用程序中实现什么有一个感觉。
框架
ASP.NET DaST 包含在一个 .dll
文件中。此框架不是附加组件,而是标准 WebForms 和 MVC 框架的完整替代方案。同时,ASP.NET DaST 不拒绝任何标准功能,并且可以逐页平滑集成到现有的 ASP.NET 应用程序中。
HTML 模板
假设我们有一些用户订单项数据,我们想以树形形式输出。DaST 模板可以是以下内容
<div dast:scope="CustomerRepeater" class="indent"> <b>Customer:</b> {CustomerName} <div dast:scope="OrderRepeater" class="indent"> <div><b>Order:</b> {OrderID}</div> <div dast:scope="ItemRepeater" class="indent"> <div><b>Item:</b> {ItemName}</div> </div> </div> </div>
通常,为了输出用户订单项数据,我们需要 3 个嵌套的重复器。DaST 模板改为拥有 3 个嵌套的范围。此外,这是清晰纯粹的 HTML,没有任何特殊的标记或控制流语句!
控制器
控制器告诉系统如何渲染模板。这是我们之前模板控制器的一部分:
public class NonsenseExample1aController : ScopeController { private void DataBind_CustomerRepeater() { var customers = DataLayer.GetCustomers(); // get customers CurrPath().RepeatStart(); // zeroize repeater foreach (var c in customers) // loop through customers { CurrPath().Repeat(); // repeat scope for current customer CurrPath().Replace("{CustomerName}", c.GetValue("Name")); // render name CurrPath("OrderRepeater").Params.Set("CUST", c); // pass to next scope } } private void DataBind_OrderRepeater() { ... } private void DataBind_ItemRepeater() { ... } }
DataBind_CustomerRepeater()
处理程序控制模板中的CustomerRepeater
范围。它只是检索客户对象并为每个客户重复该范围,在每次迭代时将{CustomerName}
占位符替换为其真实值。另外两个处理程序,用于OrderRepeater
和ItemRepeater
范围,将是类似的。
动作
动作机制是 DaST 对事件的替代。动作在客户端使用 JavaScript DaST.Scopes.Action(..)
函数触发
<a href="javascript:DaST.Scopes.Action('...', 'SubmitInput', ['abc', 123])"> Click here to raise "SubmitInput" action </a>
此代码触发SubmitInput
动作,该动作将由控制器类中实现的动作处理程序处理。最后一个参数是与动作一起传递的一些通用数据
private void Action_SubmitInput(object arg) { string p1 = (string)((object[])arg)[0]; // "abc" int p2 = (int)((object[])arg)[1]; // 123 // processing goes here ... }
双工动作/消息
DaST 最酷的功能之一是你可以向相反方向(从服务器到客户端)触发动作,并在 JavaScript 中处理它们!这种机制称为双工消息传递,这里有一个示例
private void Action_SubmitInput(object arg) { // processing goes here ... CurrPath().MessageClient("InputSubmitted", new object[] {"abc", 123}); }
这会将InputSubmitted
消息以及通用参数发送到客户端。在页面上,我们使用 JavaScript 处理此消息
<script language="javascript" type="text/javascript"> DaST.Scopes.AddMessageHandler('...', 'InputSubmitted', function(data) { var p1 = data[0]; // "abc" var p2 = data[1]; // 123 // processing goes here }); </script>
AJAX 和局部更新
DaST 拥有最简单、最强大的基于区域的原生 AJAX 支持,我们可以字面上更新我们指向的任何页面区域,而无需额外的代码或标记。
private void Action_SubmitInput(object arg) { // processing goes here ... CtrlPath("CustomerRepeater", 2, "OrderRepeater").Refresh(); }
因此,我们只需指向某个范围并对其调用 Refresh()
- 就是这样!这会使系统重新调用必要的绑定处理程序,生成局部输出,并在客户端更新相应的容器。
控制器动作
在一般情况下,DaST 页面由多个控制器(和多个嵌套模板)组成,它们之间可以相互通信。这就是控制器动作机制发挥作用的地方。下面是触发动作以与子或父控制器通信的示例
private void Action_SomeActionName(object arg) { // some processing can go here CurrPath().RaiseAction("HelloFromChild", new object[] { "abc", 123 }); CurrPath("SomeChildScope").InvokeAction("HelloFromParent", new object[] { "abc", 123 }); // other processing can go here }
与客户端动作一样,我们可以随控制器动作传递通用参数。然后HelloFromChild
和HelloFromParent
动作可以分别在父控制器和子控制器中处理
private void Action_HelloFromChild(object arg) // or Action_HelloFromParent for child controller { string p1 = (string)((object[])arg)[0]; // "abc" int p2 = (int)((object[])arg)[1]; // 123 // processing goes here }
这里的语法与客户端动作处理程序完全一致。
总结
DaST 在所有方面都超越了 WebForms,并且相对于最新的 MVC 框架也具有重要优势。只需将页面分成嵌套的矩形并操作它们以达到所需的输出!这才是 Web 开发应该有的样子。不需要大量的服务器控件、奇怪的页面生命周期、笨重的绑定表达式或类似的东西。纯 HTML 模板可以存储在任何地方,提供原生的 CMS 功能。此外,你还可以获得最简单、最强大的基于区域的原生 AJAX 支持,你可以字面上更新你指向的任何页面区域,而无需额外的代码或标记。这个概念非常新颖独特,而且它听起来就那么简单。
对于那些懒得学习新框架的人,DaST 真的没什么可学的。你只需大约 2 小时——也就是阅读本教程的时间——就能学到 DaST 开发的所有方面。它是一个简单的概念,一些理论,十几个 API 函数,以及真正直观的编程——这就是全部。
链接
以下是你可能需要的一些有用链接:
- 项目网站是 www.Makeitsoft.com
- 从 CodePlex 下载所有源代码:aspnetdast.codeplex.com
- 支持、问题、反馈:DaST 支持论坛或联系我们
- 关注@rgubarenko 获取最新新闻和更新
现在是时候转到实际的教程了。它为您提供了所有必要的概念理论,然后是编码演练,其中包含可以在本地计算机上下载和运行的真实应用程序示例。希望您喜欢!
目录
从顶层来看,本教程分为 3 个部分
- 概念 - 为您提供所有必要的模式理论知识
- 编码基础 - 逐步讲解 LittleNonsense 演示应用程序第一部分的代码
- 高级编码 - 解释高级功能并逐步讲解 LittleNonsense 演示的 Web 2.0 部分
LittleNonsense 演示
为了演示 DaST 技术的实际应用,我创建了一个 LittleNonsense DEMO Web 应用程序。顾名思义,这个应用程序在现实世界中意义不大;但是,它完美地演示了 DaST 模式的强大功能和框架的所有特性。我建议您下载源代码并在本地机器上运行此应用程序。
此演示应用程序的总体功能非常简单。它从 XML 文件中读取客户订单数据,并以用户友好的形式输出到网页。下面的清单显示了位于 /App_Data
文件夹中的此源 XML 数据文件的一部分
清单 0.1: ../App_Data/Data/LittleNonsense.xml 数据源 XML 文件的一部分
01: <?xml version="1.0" encoding="utf-8"?> 02: <LittleNonsense> 03: <Customer id="C01" name="John"> 04: <Order id="O01" date="2012-01-01"> 05: <Item id="I01" name="IPod Touch 16GB" /> 06: <Item id="I02" name="HDMI Cable" /> 07: </Order> 08: </Customer> 09: <Customer id="C02" name="Roman"> 10: <Order id="O02" date="2012-04-05"> 11: <Item id="I03" name="ASUS/Google Nexus 7 Tablet" /> 12: <Item id="I04" name="Nexus 7 Protective Case" /> 13: </Order> 14: <Order id="O03" date="2012-04-22"> 15: <Item id="I05" name="ASUS EEEPC Netbook" /> 16: <Item id="I06" name="8 Cell Battery" /> 17: </Order> 18: </Customer> 19: ............... 20: </LittleNonsense>
如您所见,XML 数据文件的结构非常简单。我们有一组客户。每个客户可以有多个订单。每个订单由多个订单项组成。每个元素都有一个唯一的 id
属性和第二个属性,其中包含一些有意义的数据:客户名称、订单日期或商品名称。
我将 LittleNonsense 应用程序分为两个独立的部分:示例 1 和示例 2,分别在 NonsenseExample1.aspx
和 NonsenseExample2.aspx
页面上实现。示例 1 非常简单,用于概念解释和本教程的编码基础部分。示例 2 包含更多内容,并演示了 Web 2.0 和 DaST 框架的所有高级功能。示例 2 用于本教程的高级编码部分,我们稍后会讲到。现在,让我们介绍示例 1。
示例 1 介绍
本示例旨在让您感受 DaST 渲染机制,并向您展示 DaST 概念的独特之处。具体而言,示例 1 涵盖以下主题:
- DaST 应用设计基础
- 带嵌套重复器的简单输出
此应用程序所做的全部工作就是从 XML 文件中读取一些分层数据,并使用 3 个嵌套的重复器将其输出。下面是示例 1 输出的屏幕截图
图 EX1:NonsenseExample1.aspx 页面输出的屏幕截图
因此,我们只需重复客户、每个客户的订单以及每个订单的商品,以用户友好的分层树形式显示结果。XML 文件中每个元素的 id
属性都显示在实际文本数据之前的方括号中。在后续部分中,我将详细解释示例 1 的完整源代码。现在,我想从一些理论知识和 DaST 概念的解释开始。
1. DaST 概念
首先,DaST 不仅仅是一个框架,它是一种全新的 Web 开发模式和独特的应用程序设计概念。ASP.NET DaST 项目是此模式的 .NET 实现。但该模式可以为任何其他平台(如 PHP (即将推出的项目)、JSP 等)实现。该概念源于一个非常简单的想法:每个网页都不过是服务器端逻辑生成并呈现在客户端浏览器中的一堆数据。整个页面渲染过程可以分为两个步骤:
- 生成一堆数据值
- 使用 HTML 标记呈现这些值
为完成这些步骤,DaST 使用了“模板”和“控制器”的组合。“控制器”是一个后端类,用于生成值。“模板”是一个符合 W3C 标准的有效 HTML 文件,值将被插入其中。这就像听起来那么简单:
- 开发人员实现一个控制器类来生成一组字符串值
- DaST 将这些值插入到相应的 HTML 模板中
- 生成的 HTML 输出被发送回客户端浏览器
除了这种方法的明显简单性和灵活性之外,HTML 模板还为我们提供了理论上最大的演示分离!这里的核心问题是渲染过程中控制器和模板之间发生了什么,以及值是如何传递到 HTML 模板中的特定位置的……这正是 DaST 引擎的作用!DaST 代表数据作用域树 (data scope tree)。你很快就会明白为什么,但在深入了解更多细节之前,让我用一张简单的图片来阐释 DaST 渲染方法,你可以将其用作参考
图 1.1:DaST 渲染过程的顶层设计
此图可视化了 DaST 模式的全部要点,并说明了使用模板/控制器组合获取页面输出。现在,我将解释图 1.1 并给出该概念的正式定义:
- 该模式的核心思想是,每个网页都可以视为一组随机嵌套的独立功能单元——数据作用域。由于数据作用域的层次结构,整个网页就变成了一个数据作用域树(因此得名“DaST”)。
- 页面输出完全通过HTML 模板进行控制,该模板也可以分解为数据作用域的树。HTML 模板中数据作用域的物理表示是包含在带有
dast:scope
属性的DIV
或SPAN
容器元素中的 HTML 标记片段。将页面分解为数据作用域并选择正确的树结构由开发人员决定,但通常数据作用域包含一些紧密相关的 UI 元素。除了常规 HTML 标记外,数据作用域还可以有占位符,如{SomeValue}
,在渲染时会被真实数据值替换。占位符是值从控制器类传递到的特定位置。 - 控制器类通过一组范围绑定处理程序为每个数据范围单独生成值——一个处理程序对应一个或多个数据范围。绑定处理程序的职责是操作范围并将其内部的占位符绑定到真实值。
- 事件处理由“动作”机制替代。除了绑定处理程序,控制器还可以为客户端或父控制器或嵌套控制器触发的相应动作实现一组“动作处理程序”。
这是顶层概念定义。接下来我们将仔细研究作用域树。
1.1. 范围树
范围树本质上是一种机制,赋予开发人员对页面输出的完全控制。在“页面渲染过程”开始时,DaST 通过解析 HTML 模板构建范围树。这种树结构通过范围树 API 暴露给开发人员,因此开发人员在控制器类的动作和绑定处理程序中的任务就简化为指向树中的特定范围并对其进行操作:将真实值带入占位符,重复范围内容等等。范围树结构不是一成不变的。一旦树从 HTML 模板中最初解析,它可能会在后续的渲染过程中发生变化。例如,当某个范围被重复时,树会从该范围生成一个额外的分支。我们将区分范围树的两种主要状态:初始范围树和渲染后的范围树。
初始范围树
初始作用域树是在渲染过程开始时从 HTML 模板解析的树。换句话说,初始树是 HTML 模板的结构骨架。模板定义了初始树,反之,拥有初始作用域树,我们可以构建由仅包含作用域容器元素的有效且功能齐全的 HTML 模板。为了更好地理解,我将描绘我谈论的所有作用域树。我将使用以下符号:
- 数据范围由内部带有范围名称的黑色矩形表示。
- 从范围中出来的连接线表示范围内容。
- 带有“重复”标签的红色箭头表示范围内容被重复。
- 灰色矩形和连接线显示在渲染过程中添加到树中的分支。
- 蓝色文本或标记用于注释和解释。
现在让我们为我们的示例 1 创建初始范围树。为了实现示例 1 介绍部分中图 EX1 所示的 UI,显然我们需要 3 个嵌套的重复器。在 DaST 术语中,这是一个包含 3 个嵌套范围的树,这些范围重复它们的内容。图 1.2 显示了我提出的初始范围树
图 1.2:LittleNonsense 演示示例 1 部分的初始范围树
需要注意的几点重要事项:
- 根据定义,每个数据作用域树都以
NULL
作用域作为其根。如果模板中没有作用域,则数据作用域树由一个单独的NULL
作用域组成。 - 从
ItemRepeater
范围出来的连接线末端没有叶子节点。这意味着ItemRepeater
范围有内容,但没有嵌套的子范围。
现在让我们谈谈渲染后的作用域树。
渲染后的范围树
渲染后的作用域树是初始树在渲染过程结束时所变成的树。换句话说,渲染后的树代表了最终的网页。它总是比初始树大,因为由于重复作用域而出现了新的分支。
回想图论,我们可以得出几个观察结果:
-
初始树始终是渲染后的作用域树的子树。
-
渲染后的树可以通过重复初始树中的作用域获得。
回到我们的示例 1。查看示例 1 介绍部分的图 EX1 上的 UI 和图 1.2 上的初始树,现在让我们描绘渲染后的范围树
图 1.3:LittleNonsense 演示示例 1 部分的渲染后范围树
初始树,即渲染后的作用域树的子树,以黑色表示。灰色部分是所有所需作用域重复后为获得所需输出而添加的所有内容。
蓝色注释帮助你理解渲染树中的“范围”如何对应于示例 1 页面的最终输出。XML 数据文件中有 3 个客户,因此 CustomerRepeater
内容为客户“John”、“Roman”和“James”重复 3 次。客户“Roman”有 2 个订单,因此 OrderRepeater
内容为订单 ID“[O02]”和“[O03]”重复 2 次。依此类推。
范围路径
如前所述,开发人员通过范围树 API 操作范围来控制页面输出。在操作特定范围之前,必须使用“范围路径”来选择它。“范围路径”只是在树中寻址范围的一种方式。每个数据范围都由其范围路径唯一标识。当网页渲染时,范围路径的字符串版本用于相应范围容器元素的id
属性中。范围路径的唯一性保证了范围容器元素id
属性的唯一性。
范围路径由“段”组成。“段”是基于 0 的“重复轴”和范围名称的组合。“重复轴”表示容器范围的重复迭代。如果父容器范围不重复其内容,则重复轴保持为 0。
在示例 1 结果页面的 HTML 源代码中,范围容器元素如下所示
<div id="SCOPE$0-CustomerRepeater$1-OrderRepeater$0-ItemRepeater">
id
属性包含此范围路径的字符串表示。实际路径在“SCOPE”前缀之后,段由“$”符号分隔。NULL
范围始终相同,因此我们不将其包含在范围路径中。
查看上面的id
属性,我们可以立即知道这个DIV
在图 1.3中对应哪个范围。只需沿着路径走:取第一个(轴 0)CustomerRepeater
,然后取第二个(轴 1)分支到OrderRepeater
,然后取第一个分支到ItemRepeater
。所以,这条路径指向带有“[O02] 2012-04-05”文本的ItemRepeater
范围(参见图 1.3)。
请注意,范围容器 id
属性总是使用绝对路径,但在控制器回调中,我们也可以使用相对范围路径。相对路径从当前范围开始,即当前动作或绑定处理程序正在执行的范围。
1.2. 控制器
控制器是一个后端类,它告诉系统如何渲染作用域树。作用域树由与控制器关联的模板定义。你可以将 DaST 控制器和模板组合视为标准 ASP.NET 服务器控件或 MVC 视图/控制器的概念性替代。每个控制器都可以包含其他嵌套控制器,每个控制器都负责其自身部分的作用域树。
为了控制树的所需部分,控制器需要“附加”到初始作用域树中的特定作用域。当控制器附加到作用域时,它就“负责”从该作用域开始渲染部分作用域树(或子树)。子树由渲染遍历过程渲染,该过程依次访问作用域,调用相应的绑定处理程序,并生成输出(更多详细信息请参见页面渲染部分)。将控制器附加到其作用域是树模型构建的一部分,这发生在渲染过程的开始,所以我们这里只讨论初始作用域树。这在调用任何动作或绑定处理程序之前完成,因此作用域树仍然具有从模板解析的初始结构。默认情况下,DaST 页面由一个名为“根控制器”的顶层控制器驱动,该控制器与“根模板”关联。根控制器始终附加到此模板定义的树的 NULL
作用域。如果没有其他控制器,根控制器将“负责”渲染整个作用域树。
例如,在示例 1 应用程序中,为了简化,我使用了一个负责整个范围树的单个顶层根控制器。但是,如果我将某个子控制器附加到图 1.2 上的 OrderRepeater
范围,并且渲染遍历超出了 OrderRepeater
,那么负责的控制器将是附加到 OrderRepeater
的控制器,而不是附加到 NULL
范围的根控制器。并且此控制器将用于执行动作和绑定处理程序。
所有相应的编码技术都在控制器类部分进行了深入解释。
范围树 API
范围树 API 是用于在控制器类中的动作和绑定处理程序内部与范围树进行交互的接口。为了获得所需的页面输出,我们的任务就简化为操作树中的特定范围。在使用范围路径(参见范围路径)指向范围之后,我们可以对其执行某些操作,例如:
-
将值绑定到范围内的占位符。
-
显示、隐藏或重复范围的内容。
-
刷新范围(DaST-AJAX 局部更新)。
-
获取或设置范围的通用名称-值参数
- 以及其他一些……
更多详情请访问范围树 API 参考。
1.3. 页面渲染
DaST 页面“渲染过程”是指从客户端请求到达页面到响应发送回客户端的这段时间。在标准 ASP.NET 中,我们通常称之为“页面生命周期”,但我在这里故意不使用这个术语,以强调其不同之处。没有奇怪的页面和子控件事件序列,执行来回跳转,没有关于使用哪个事件来完成特定任务的问题,完全没有歧义。DaST 渲染过程以数学精度定义,它是 DaST 模式中最美妙的部分。
客户端-服务器工作流
让我们从客户端-服务器交互和请求处理流的顶层图开始。下面是显示此流分步进行的图表
图 1.4:顶层请求处理流程
因此,页面最初加载,然后响应客户端上的一些事件多次重新加载。总体工作流看起来相当标准,除了使用 DaST 页面而不是常规页面。下面是图 1.4 中红色圆圈标记的步骤的更详细描述:
-
1. 用户导航到页面。初始页面加载请求发送到服务器。
-
2. 请求处理交给特定的控制器。渲染过程执行。
-
对于初始页面加载,渲染过程输出整个页面。
-
-
3. 响应就绪,初始页面输出显示在用户浏览器中。
-
4. 用户继续在此页面上工作,并调用一些客户端操作,从而发起另一个请求。
-
5. 请求处理交给特定的控制器。渲染过程执行。
-
这次,由于存在客户端动作,该动作首先被处理。然后渲染过程部分输出页面,仅针对请求刷新的范围。
-
-
6. 服务器响应中的刷新内容应用于已刷新的特定范围。页面在用户浏览器中更新。
-
步骤 4-6 重复,直到用户导航到另一个页面。
这里有几点需要注意。首先,DaST 中没有完全页面回发这种东西。DaST 中的每个客户端动作都会导致页面上特定范围的部分刷新。无论你希望更新什么,都需要将其包装到数据范围中。
其次,在代码隐藏设计方面,DaST 局部更新比基于 UpdatePanel
控件的标准 AJAX 具有巨大优势。标准异步回发会重新执行所有代码,除非被丑陋的 IsAsyncPostBack
条件包围。DaST 只重新执行刷新范围的绑定处理程序!所有详细信息都在下一节中。
DaST 渲染过程
现在让我们深入研究页面渲染过程,看看幕后发生了什么。像 DaST 中的所有其他事物一样,渲染过程基于模板定义的范围树。请注意,图 1.1 中所示的渲染过程有点简化,因为只有一个根控制器负责整个范围树。在实际应用程序中,一个页面通常由多个嵌套控制器渲染,每个控制器都负责其自身部分范围树,即范围子树(参见控制器部分)。因此,现在让我们将渲染过程形式化。在顶层,它可以分为 3 个步骤:
- 步骤 1:客户端动作处理 – 系统通过调用控制器类中实现的相应动作处理程序来处理客户端动作。
- 步骤 2:选择起始范围 – 系统在树中选择特定范围,子树将从该范围开始渲染。
- 步骤 3:树渲染遍历 – 最重要的步骤,实际渲染范围树发生在此处。遍历过程以“严格定义的遍历顺序”逐个遍历当前子树中(从选定的起始范围开始)的所有“范围”。遍历算法是“带有自上而下中序遍历的严格后序遍历”。这意味着树中的数据范围按照它们对应的容器元素在模板中遇到的相同顺序被访问——这正是我们获得正确页面输出所需要的。
现在让我们详细说明系统在这些步骤中实际做了什么。这必须分开进行,分别针对初始和后续页面加载(参见工作流部分),因为在这两种状态下,步骤的执行方式存在一些差异。因此,首先,假设我们正在进行初始页面加载。这种情况下的渲染步骤如下:
- 步骤 1:客户端动作处理
此步骤被跳过,因为初始页面加载没有动作。 - 步骤 2:选择起始范围
对于初始页面加载,需要渲染整个范围树,因此起始范围将是范围树的根范围,即NULL
范围。 - 步骤 3:树渲染遍历
从根范围开始,系统按遍历顺序遍历范围树。对于每一个访问的范围,DaST 检索负责的控制器(回忆控制器部分)并执行以下子步骤。我强调以下子步骤针对当前子树中的每个范围执行- 步骤 3.1:控制器模型准备 – 范围树与控制器类中的处理程序相关联。如果此控制器的模型已准备好,则跳过整个子步骤。此条件确保了按需模型设置:每当遍历遇到具有未设置控制器模型的范围时,准备过程就会执行。此子步骤进一步分解为更小的步骤
-
步骤 3.1.1:选择特定的模板/控制器对进行请求处理。
-
步骤 3.1.2:HTML 模板被解析成范围树内部数据结构。
-
步骤 3.1.3:调用控制器设置方法以将处理程序与范围树关联(参见控制器编码部分的示例)。
-
-
步骤 3.2:范围渲染输出 – 当前范围被渲染,结果输出被添加到页面输出中。此子步骤是调用绑定处理程序的地方,它分解为以下更小的步骤:
-
步骤 3.2.1:控制器中调用相应的绑定处理程序。
-
步骤 3.2.2:在绑定处理程序内部,我们使用范围树 API 执行多项操作:
-
将真实值插入占位符
-
按所需次数重复范围
-
操作范围,设置参数,隐藏,显示等。
-
-
步骤 3.2.3:生成范围输出并添加到页面输出中。
-
- 步骤 3.1:控制器模型准备 – 范围树与控制器类中的处理程序相关联。如果此控制器的模型已准备好,则跳过整个子步骤。此条件确保了按需模型设置:每当遍历遇到具有未设置控制器模型的范围时,准备过程就会执行。此子步骤进一步分解为更小的步骤
-
我们完成了!我们的页面已完成,输出已发送到客户端浏览器。
现在,假设我们正在进行后续页面加载。那么渲染步骤如下所示:
-
步骤 1:客户端动作处理
这次我们确实有一个客户端动作。整个动作机制在动作部分有描述。简而言之,动作处理程序在某个控制器内部实现。为了选择这个控制器,DaST 需要知道范围树的结构。因此,在处理动作之前,系统会尝试通过对控制器路径上的范围执行步骤 3.1 来构建范围树的部分模型。树恢复后,动作处理程序在目标控制器上被调用。 -
步骤 2:选择起始范围
对于后续页面加载,范围树需要部分渲染,从动作处理程序中明确刷新的范围开始。可以有 0 个或多个刷新的范围。在多个刷新的范围情况下,每个范围都成为相应子树遍历的起始范围。 -
步骤 3:树渲染遍历
如果没有范围刷新,则跳过此步骤。否则,所有内容都与初始页面加载的步骤 3 完全相同,唯一的区别是 DaST 必须渲染多个子树,而不是一个大的范围树。系统依次遍历子树,重新调用适当的绑定处理程序。每个子树都会生成一个打包到响应中的输出,以及来自其他子树的输出,然后响应被发送回客户端。 -
我们完成了!我们的页面已完成,输出已发送到客户端浏览器。在多个输出的情况下,它们由客户端脚本拾取并应用于相应的范围容器元素,以实现标准 AJAX 局部更新行为。
就是这样!听起来令人难以置信,但这种简单、清晰、透明的方法可以渲染任何页面,无论它多么复杂!看看 DaST 渲染过程与标准 ASP.NET 渲染、奇怪的页面生命周期、事件优先级、大量带有嵌套数据绑定委托的服务器控件等等相比,是多么的干净、统一和定义明确!即使 MVC 作为 WebForms 的巨大进步,并且在服务器端组织得非常好,但它在视图和部分视图中使用的控制流和代码生成构造立即扼杀了任何实现适当演示分离的希望。DaST 模式没有这些问题。它一次性抛弃了所有这些复杂性!
控制器中的渲染
由于开发人员只处理控制器类,我们也想从控制器的角度来看渲染过程。在控制器类内部,执行可以分为 3 个独立阶段:
- 阶段 1:控制器模型准备 – 执行步骤 3.1:控制器模型准备。
- 阶段 2:动作处理 – 执行步骤 1:客户端动作处理。此阶段仅在后续页面加载时发生,并跳过初始页面加载。
- 阶段 3:范围树渲染输出 – 假设控制器负责特定的子树,则对该子树中的每个范围按遍历顺序执行步骤 3.2:范围渲染输出。
基于这些简单的阶段,我们始终知道控制器内所有函数的精确执行顺序。您将在随后的编码演练部分中看到这是如何工作的。
目前,概念理论部分已完成!至此,我们已掌握足够的知识,可以开始学习真实的编码示例并讨论 DaST 开发技术。还有一些理论,但我们将在本教程的代码示例中学习其余部分。
2. 编码基础
为了学习 DaST 编程的基础知识,我们将逐步讲解 LittleNonsense 演示应用程序的示例 1 部分的代码。为了更深入的理解,我建议您下载源代码并在所有解释前将其放在您面前。此外,本教程中属于范围树 API 的所有函数后面都有一个“[*]”链接,可直接将您带到该函数的 API 参考。
那么,现在开始编码吧。在做任何事情之前,我们需要引用 DaST 框架。ASP.NET DaST 包含在一个 AspNetDaST.dll
中,您必须将此 DLL 放到 Web 应用程序的 /Bin
文件夹中。来自 范围树 API 的所有类都位于 AspNetDaST.Web
命名空间中。
2.1. DaST 页面
DaST 中的请求 URL 指向物理 .aspx
页面,这与 WebForms 类似,但与 MVC 基于模式的请求路由不同。.aspx
文件的内容被忽略——该页面仅作为请求入口点。在访问页面后,请求处理工作流将转到DaST 渲染引擎。换句话说,DaST 框架覆盖了标准 ASP.NET 页面的整个渲染过程。
现在看看示例 1。请求命中了NonsenseExample1.aspx
页面,相应代码后置的目的就是将流程转移到特定的 DaST 控制器。下面是代码后置的源代码
清单 2.1: NonsenseExample1.aspx.cs 文件
1: public partial class NonsenseExample1 : AspNetDaST.Web.DaSTPage 2: { 3: protected override AspNetDaST.Web.ScopeController ProvideRootController() 4: { 5: return new NonsenseExample1Controller(); 6: } 7: }
所以,要将常规的.aspx
页面转换为 DaST 页面,你需要做以下几件事:
-
创建标准的
.aspx
页面并保持默认(其内容无论如何都会被忽略) -
页面类继承自
AspNetDaST.Web.DaSTPage
而不是System.Web.UI.Page
-
实现单个
ProvideRootController()
方法,返回根控制器实例
就是这样!现在你拥有一个由指定根控制器驱动的 DaST 页面。
此时你可能会问,为什么不直接使用基于 URL 模式的请求路由到控制器,就像 MVC 中那样,并且拥有一个中间的 .aspx
页面却什么都不做有什么目的?好的,这个设计的主要原因和巨大好处是 DaST 页面可以在一个应用程序中与标准 ASP.NET 页面共存,而无需任何额外的设置!这使得开发人员可以逐页平滑地将现有 ASP.NET 应用程序过渡到基于 DaST 的应用程序。我尝试将 DaST 平滑地集成到现有的 ASP.NET 基础设施中,因此我们仍然可以使用所有标准功能,如 HTTP 上下文、会话处理、缓存管理等,而无需更改。
2.2. HTML 模板
模板以纯文本形式传递给控制器,因此模板的物理位置可以是任何地方:文件、数据库、远程服务器等。对于 LittleNonsense 应用程序,所有模板都作为 .htm
文件存储在 /App_Data/Templates/
文件夹下。现在让我们为 示例 1 构建模板。
在 ASP.NET 和 MVC 中,我们通过逆向工程所需页面外观到一组服务器控件来构建页面和视图。DaST 也是如此——我们获取所需的 UI 并将其逆向工程到数据范围树中。在范围树部分,我们已经发现示例 1 UI 在图 EX1 上可以通过 3 个嵌套数据范围来实现。初始范围树在图 1.2 上,相应的模板在下面的列表中
清单 2.2: ../App_Data/Templates/NonsenseExample1.htm
01: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 02: <html xmlns="http://www.w3.org/1999/xhtml"> 03: <head> 04: <title>ASP.NET DaST - Little Nonsense | {TodayDate}</title> 05: .................... 06: </head> 07: <body class="some-class-{TodayDate}"> 08: .................... 09: <div dast:scope="CustomerRepeater" class="indent"> 10: <b>CUSTOMER:</b> [{CustomerID}] {CustomerName} 11: <div dast:scope="OrderRepeater" class="indent"> 12: <div><b>ORDER:</b> [{OrderID}] {OrderDate}</div> 13: <div dast:scope="ItemRepeater" class="indent"> 14: <div><b>ITEM:</b> [{ItemID}] {ItemName}</div> 15: </div> 16: </div> 17: </div> 18: </body> 19: </html>
你可以看到我们的 3 个“范围”,由带有dast:scope
属性和占位符的嵌套DIV
容器表示。在渲染输出阶段(参见渲染部分),这些范围被重复,占位符被相应的值替换,从而得到我们想要的输出。
您可能会注意到BODY
标签类和TITLE
中的{TodayDate}
占位符。它在那里只是为了演示值可以插入到模板的任何位置,包括HEAD
部分。但是,我们不能将作用域放入此部分,因为DIV
等容器元素不允许出现在HEAD
标签中。此标签拥有的任何标记都被视为NULL
作用域内容的一部分。
设计 HTML 模板是一个创造性的过程。模板可能会根据您尝试实现的 UI 类型而变得更加复杂。同时,它总是比 MVC 视图或者更甚的是标准的 .aspx
标记页面复杂得多,因为模板始终只是包含作用域容器的有效 HTML,无论您的 UI 有多复杂。
格式良好
由于整个 DaST 处理都围绕数据范围树进行,因此从模板中正确解析初始范围树非常重要(参见渲染过程中的渲染步骤 3.1.2)。因此,DaST 引擎对模板中格式不正确的 HTML 非常敏感。开发人员有责任确保 HTML 模板格式良好(所有标签都正确关闭,属性具有名称/值格式等)。如果 DaST 无法解析您的模板,引擎将抛出异常,并提供问题的更详细解释。
目前我使用基于语法的 HTML 解析器从模板中提取数据范围树。之后这可能会改为基于正则表达式的解析器,这样 DaST 对模板中的语法错误会更宽容。
生成的页面输出
在清单 2.2 中的模板完全渲染后,我们得到了图 EX1 所示的所需示例 1 页面。从此刻起,控制流返回到.aspx
页面,该页面完成输出。换句话说,DaST 不仅仅按原样输出结果页面,而是通过标准的Web.UI.Page
设施来实现。幕后,结果页面会发生以下情况:
-
页面分解为头部和主体部分
-
头部部分插入到
.aspx
页面头中 -
主体部分插入到
.aspx
页面主体字面量中 -
主体属性添加到
.aspx
页面的body
元素中
模仿标准 .aspx
页面有一些很好的理由。首先,我们需要 DaST 与标准 ASP.NET 的无缝集成。其次,为了节省实现时间并让我的生活更轻松,决定使用带有 DaST 附加组件的标准 MS AJAX 库,而不是创建我自己的 AJAX 层,因为它已经拥有所有那些回发客户端脚本和所有其他东西。
创建专用的轻量级 DaST AJAX 层已列入开发计划。一开始,我显然想尽快证明这个概念,并没有费心去开发自己的 AJAX。现在,当功能齐全的库发布后,是时候清理、完善并添加更多功能了。PHP DaST 框架可能会从一开始就获得基于 jQuery 的 AJAX 库,然后我将它移植到 ASP.NET DaST。
2.3. 控制器类
控制器是真正编程的开始。从DaST 概念部分,你应该已经对控制器类是什么以及它的作用有了很好的顶层认识。在LittleNonsense演示中,所有控制器都存储在.cs
文件中的/App_Code/Controllers/
文件夹下。从清单 2.1 中我们可以看到,NonsenseExample1.aspx
页面使用名为NonsenseExample1Controller
的根控制器类来处理所有客户端请求。首先,我将给出这个类的完整源代码,然后在接下来的部分中,我将逐行解释这段代码:
清单 2.3: ../App_Code/Controllers/NonsenseExample1Controller.cs
01: public class NonsenseExample1Controller : ScopeController 02: { 03: public override string ProvideTemplate() 04: { 05: return Utils.LoadTemplate("NonsenseExample1.htm"); // load temlate content from anywhere 06: } 07: 08: public override void InitializeModel(ControllerModelBuilder model) 09: { 10: // use one binding handler to bind all nested scopes (except for root scope) 11: model.SetDataBind(new DataBindHandler(DataBind_ROOT)); 12: model.Select("CustomerRepeater").SetDataBind(new DataBindHandler(DataBind_CustomerRepeater)); 13: model.Select("CustomerRepeater", "OrderRepeater").SetDataBind(new DataBindHandler(DataBind_OrderRepeater)); 14: model.Select("CustomerRepeater", "OrderRepeater", "ItemRepeater").SetDataBind(new DataBindHandler(DataBind_ItemRepeater)); 15: } 16: 17: // 18: private void DataBind_ROOT() 19: { 20: CurrPath().Replace("{TodayDate}", DateTime.Now.ToString("yyyy-MM-dd")); // output some example values 21: } 22: 23: private void DataBind_CustomerRepeater() 24: { 25: var customers = DataLayer.GetCustomers(); // get all customers 26: 27: CurrPath().RepeatStart(); // zeroize repeater 28: foreach (var c in customers) // loop through customers 29: { 30: CurrPath().Repeat(); // repeat scope for current customer 31: CurrPath().Replace("{CustomerID}", c.GetValue("ID")); // bind customer id 32: CurrPath().Replace("{CustomerName}", c.GetValue("Name")); // bind customer name 33: CurrPath("OrderRepeater").Params.Set("CUST", c); // pass customer to next scope 34: } 35: } 36: 37: private void DataBind_OrderRepeater() 38: { 39: var c = CurrPath().Params.Get<object>("CUST"); // customer passed in params 40: var orders = DataLayer.GetOrders((string)c.GetValue("ID")); // customer orders 41: 42: CurrPath().RepeatStart(); // zeroize repeater 43: foreach (var o in orders) // loop through orders 44: { 45: CurrPath().Repeat(); // repeat scope for current order 46: CurrPath().Replace("{OrderID}", o.GetValue("ID")); // bind order id 47: CurrPath().Replace("{OrderDate}", o.GetValue("Date")); // bind order date 48: CurrPath("ItemRepeater").Params.Set("Order", o); // pass order to next scope 49: } 50: } 51: 52: private void DataBind_ItemRepeater() 53: { 54: var o = CurrPath().Params.Get<object>("Order"); // order passed in params 55: var items = DataLayer.GetOrderItems((string)o.GetValue("ID")); // order items 56: 57: CurrPath().RepeatStart(); // zeroize repeater 58: foreach (var i in items) // loop through items 59: { 60: CurrPath().Repeat(); // repeat scope for current item 61: CurrPath().Replace("{ItemID}", i.GetValue("ID")); // bind item id 62: CurrPath().Replace("{ItemName}", i.GetValue("Name")); // bind item name 63: } 64: } 65: }
正如我之前提到的,示例 1 由一个根控制器驱动,其代码如上所示。此控制器负责从 NULL
范围开始的整个范围树。在深入研究实际代码之前,我想谈谈控制器实现结构。
顶层结构
让我们看看清单 2.3 中控制器的结构。在顶层,控制器类中的函数可以分为 3 类,这些类别严格对应于 3 个渲染阶段(参见控制器渲染部分):
-
动作处理程序 – 在阶段 2 执行
上述清单中没有动作处理程序,您可以参考清单 3.3,第 39-65 行。可以有 0 个或多个处理程序来处理客户端动作和控制器动作。为了提高源代码的可读性,我总是以“Action_”前缀加上动作名称来命名动作处理程序,例如Action_SubmitInput()
,但您可以根据自己的喜好选择名称。 -
绑定处理程序(第 18-64 行) – 在阶段 3 执行
可以有 0 个或多个处理程序,对应于当前子树中的范围。为了提高源代码的可读性,我总是以“DataBind_”前缀加上范围名称来命名绑定处理程序,例如DataBind_CustomerRepeater()
或DataBind_ROOT()
(用于控制器根范围)。上述清单总共有 4 个绑定处理程序:DataBind_ROOT()
、DataBind_CustomerRepeater()
、DataBind_OrderRepeater()
和DataBind_ItemRepeater()
。
接下来,在仔细查看之前,让我们学习一些用于控制范围树的基本 API。
需要了解的基本 API
在我们深入研究控制器源代码之前,让我们学习一些基本的 API。我之前提到,整个范围树 API 只由几个类和总共十几个函数组成。了解这些,您将能够创建任何复杂程度的应用程序。在本节中,我将重点介绍理解清单 2.3 中代码所需的最重要 API。
首先,ControllerModelBuilder
[*] 类用于处理初始范围树并在 阶段 1 设置树模型。此类的实例通过 model
参数传递给 InitializeModel()
[*] 回调。以下是使用示例:
model.Select("CustomerRepeater", "OrderRepeater").SetDataBind(new DataBindHandler(SomeHandler));
此调用由 2 个步骤组成:1) 使用 Select()
[*] 函数指向范围,2) 调用模型设置函数之一。函数 Select()
[*] 将范围路径规范作为参数,并相应地移动范围指针。路径仅由范围名称组成,因为我们处理的是初始范围树。指定的路径是相对于控制器根范围的。然后 SetDataBind()
[*] 函数将处理程序函数与指向的范围关联。您还可以调用 HandleAction()
[*] 来绑定动作处理程序或 SetController()
[*] 来将子控制器附加到选定的范围。我选择强制使用链式函数调用语法,因为它看起来非常紧凑和可读。这就是我们在阶段 1 设置树模型所需的全部内容。
其次,ScopeFacade
[*] 类用于处理渲染后的作用域树,并在阶段 2 和阶段 3 从动作和绑定处理程序内部操作作用域。它有一些函数和属性,在本教程中将详细解释所有这些成员。
与初始树相同,在调用 ScopeFacade
[*] 的任何成员之前,必须选择特定的范围。这通过 CurrPath()
[*] 和 CtrlPath()
[*] 函数完成,这些函数将范围路径(参见范围路径)规范作为可选参数,并返回指向选定范围的 ScopeFacade
[*] 实例。这两个函数之间的区别在于,CurrPath()
[*] 从当前范围开始,即当前绑定处理程序正在执行的范围,而 CtrlPath()
[*] 从控制器根范围开始。所以它们就像模型设置阶段的 Select()
[*] 函数,但在渲染后的范围树中工作。以下是使用示例:
CurrPath("CustomerRepeater", 2, "OrderRepeater").Replace("{SomeValue}", "Hello World!");
再次,使用了链式语法。第一次调用是为了指向特定的范围,接下来的调用是 ScopeFacade
[*] 成员之一。在我们的例子中,它是 Replace()
[*] 函数,它接受占位符名称和替换值。Replace()
[*] 函数有几个重载,允许不同的参数组合。请注意,这次我为 OrderRepeater
范围指定了带重复轴的路径。如果未指定轴,则视为 0。
最后,常用 API 是 Params
[*] 和 StoredParams
[*] ScopeFacade
[*] 类的属性。这两个属性允许我们将一些通用参数与作用域关联起来。本质上,Params
[*] 是一种在渲染过程中在作用域之间传递数据的方式。StoredParams
[*],顾名思义,允许我们在后续回发之间保留参数。有关这些属性的更多信息,请阅读 API 描述。重要的是要理解 DaST 使用相同的控制器实例来处理其负责的整个子树,我们不应在控制器内部创建任何实例成员来存储任何数据。所有数据都必须存储在外部,如果需要在作用域之间传递对象,则应使用 Params
[*] 属性。Params
[*] 属性的类型为 ParamsFacade
[*],它有一组重载函数来获取和设置作用域参数。
好的,现在我们已经掌握了足够多的知识,可以开始深入研究代码了。
源代码详情
控制器函数的执行顺序严格由渲染过程和遍历流定义(参见渲染过程)。当系统通过此过程并遍历范围树时,负责控制器中的回调函数将依次调用,直到页面输出完成。在本节中,我将按照控制器函数的执行顺序解释所有控制器函数。在继续阅读之前,您需要回顾渲染过程和控制器渲染部分,因为我将经常引用它们。下面我将按照控制器渲染阶段,列出按执行顺序的回调,并描述它们的功能和工作原理。
一切都始于客户端请求命中NonsenseExample1.aspx
页面。示例 1 不使用动作,所以我们只讨论初始页面加载。列表 2.1 中的代码后台逻辑将所有后续处理转移到列表 2.3 中给出的NonsenseExample1Controller
类。
执行从阶段 1:控制器模型准备开始。正如我在渲染过程的步骤 3.1 中提到的,只要遍历过程命中负责控制器模型尚未设置的作用域,控制器准备就针对每个控制器执行一次。因此,对于我们尚未设置模型的控制器,DaST 需要调用 2 个设置函数:
-
系统在第 3 行调用
ProvideTemplate()
[*]。
重写此函数的目的是将特定模板作为纯文本返回。在第 5 行,我使用实用函数从站点上的目录中按名称读取模板并返回它。这种简单的方法很酷,因为我们可以将模板存储在任何地方并构建灵活的 CMS 引擎。模板返回后,系统继续执行下一个函数调用:
此函数承担着将作用域树与控制器关联的重要任务。在上一小节中,我解释了如何使用ControllerModelBuilder
[*]类来设置树模型。因此,在第 11 行,我们将DataBind_ROOT()
与控制器根作用域关联,在本例中它与NULL
作用域相同。请注意,我省略了Select()
[*]调用,因此指针停留在控制器根目录。接下来,在第 12 行,我们将DataBind_CustomerRepeater()
处理程序与CustomerRepeater
作用域关联。为了指向CustomerRepeater
作用域,我们使用Select()
[*]函数并传递其相对路径。在第 13-14 行,我们对OrderRepeater
和ItemRepeater
作用域执行相同的操作。这次路径包含多个段。
此时,模型设置完成,系统转向阶段 2:动作处理。在示例 1 中,我们没有任何动作,因此此阶段被简单跳过。这里不会调用任何回调。动作是一个更高级的主题,您将在本教程的第二部分中了解它的工作原理。最后,系统进入阶段 3:作用域树渲染输出。这是实际渲染发生并将我们的作用域树转换为图 1.3 上的渲染作用域树的阶段。在此阶段,遍历逐个访问作用域,调用绑定处理程序,并输出结果。因此,遍历从访问树的NULL
作用域开始:
- 系统在第 18 行调用
DataBind_ROOT()
。
当前路径是SCOPE
。
为了更好地理解,我将始终指定当前作用域路径,即在不带参数调用的情况下CurrPath()
[*]指向的路径。这次当前路径是SCOPE
,这意味着CurrPath()
[*]指向NULL
作用域。DataBind_ROOT()
处理程序只有一行代码。在上一小节中,我解释了如何使用ScopeFacade
[*]类从动作和绑定处理程序内部操作作用域。因此,在第 20 行,我们不带参数使用CurrPath()
[*]来指向当前作用域,即根作用域。然后我们对其调用Replace()
[*]来将格式化日期输出到{TodayDate}
占位符中。回想一下,此占位符在列表 2.2 中的模板中用于属于NULL
作用域的TITLE
和BODY
标签内部。我们在这里完成了。下一个遍历的作用域是CustomerRepeater
:
- 系统在第 23 行调用
DataBind_CustomerRepeater()
。
当前路径是SCOPE$0-CustomerRepeater
。
CustomerRepeater
作用域的内容需要为每个客户重复。{CustomerID}
和{CustomerName}
占位符需要在每次迭代中替换为相应的客户数据。因此,在第 25 行,我们检索客户列表。我们使用数据层实用程序从 XML 返回实体。然后我们对当前作用域调用RepeatStart()
[*]以将重复轴计数器重置为 0。此函数必须始终在重复作用域内容之前调用,因为默认情况下每个作用域始终有 1 个轴。在第 28-34 行,我们循环遍历所有客户对象,重复作用域,并替换占位符。循环为客户“John”、“Roman”和“James”迭代 3 次。在第 30 行,调用Repeat()
[*]函数,指示系统重复指向的作用域内容一次。此函数必须在当前迭代中的任何其他操作之前调用。在第 31-32 行,我们使用已经熟悉的语法将值粘贴到当前作用域中的{CustomerID}
和{CustomerName}
占位符中。在第 32 行,我们指向OrderRepeater
作用域并使用Params
[*]属性将客户对象作为参数传递给此作用域。在上一小节中,我解释了Params
[*]属性的工作原理。在此步骤中保存的客户将在执行到达OrderRepeater
绑定处理程序时检索,我们将知道要显示哪些订单。因此,遍历访问的下一个作用域是OrderRepeater
:
- 系统在第 37 行调用
DataBind_OrderRepeater()
。
当前路径是SCOPE$0-CustomerRepeater$0-OrderRepeater
。
这里我们需要做与之前几乎相同的事情,但这次是针对客户订单。在第 39 行,我们首先检索从上一个遍历步骤传递的客户对象。当前客户对象是“John”的。获取客户后,我们使用数据层获取他的订单列表。然后,在第 42 行,我们通过调用RepeatStart()
[*]再次准备作用域以进行重复。最后,在第 43-49 行,我们执行与上一个绑定处理程序类似的循环,但这次是针对客户订单,将占位符替换为特定的订单 ID 和日期值。在迭代结束时,我们再次使用Params
[*]来保存ItemRepeater
作用域的订单对象。我们在这里完成了,遍历继续到ItemRepeater
作用域:
- 系统在第 52 行调用
DataBind_ItemRepeater()
。
当前路径是SCOPE$0-CustomerRepeater$0-OrderRepeater$0-ItemRepeater
。
这里一切都与之前相同,但现在是针对ItemRepeater
。首先,我们从作用域参数中检索订单对象。然后从 XML 获取订单项。然后循环遍历它们,重复作用域,并将占位符替换为项值。这次我们不需要在Params
[*]中保存任何内容,因为没有更多作用域了。现在查看图 1.3 上的作用域树。此时我们已经完全遍历了第一个分支,如果只有一个客户,遍历将在这里停止。但由于我们有多个重复的客户,遍历必须遍历所有分支。因此,我们下次访问的作用域又是OrderRepeater
,但位于下一个重复的分支上:
- 系统在第 37 行调用
DataBind_OrderRepeater()
。
当前路径是SCOPE$0-CustomerRepeater$1-OrderRepeater
。
请注意当前路径中OrderRepeater
作用域的重复轴 – 它是 1,而不是 0,因为我们现在在第二个分支上!在第 39 行检索的客户对象现在是“Roman”,即在之前的CustomerRepeater
作用域的第二次重复迭代中保存的客户对象。其他一切都相同 – 获取当前客户的订单列表并重复作用域以显示它们。在同一分支上继续遍历,下一个访问的作用域是ItemRepeater
:
- 系统在第 52 行调用
DataBind_ItemRepeater()
。
当前路径是SCOPE$0-CustomerRepeater$1-OrderRepeater$0-ItemRepeater
。
和以前一样,首先,我们检索“O02”的已保存订单对象。然后获取其 ID 为“I03”和“I04”的项。然后为每个项重复作用域并替换占位符。下一个访问的作用域又是ItemRepeater
:
- 系统在第 52 行调用
DataBind_ItemRepeater()
。
当前路径是SCOPE$0-CustomerRepeater$1-OrderRepeater$1-ItemRepeater
。
它又是同一个作用域,但作用域路径不同。我们在这里,因为客户“Roman”有 2 个订单,我们现在需要输出订单“O03”的项。因此,当前路径中保存的订单对象对应于“O03”,我们重复它 2 项。此分支完成,遍历返回到OrderRepeater
:
- 系统在第 37 行调用
DataBind_OrderRepeater
。
当前路径是SCOPE$0-CustomerRepeater$2-OrderRepeater
。
当前作用域的重复轴是 2,因为我们现在在客户“James”的第三个分支上。其他一切都与之前相同,下一个作用域是ItemRepeater
:
- 系统在第 52 行调用
DataBind_ItemRepeater()
。
当前路径是SCOPE$0-CustomerRepeater$2-OrderRepeater$0-ItemRepeater
。
同样,除了路径不同,功能与之前访问的ItemRepeater
作用域类似。最后,遍历再次访问ItemRepeater
,因为“James”还有一个订单:
- 系统在第 52 行调用
DataBind_ItemRepeater()
。
当前路径是SCOPE$0-CustomerRepeater$2-OrderRepeater$1-ItemRepeater
。
我们完成了!我们的树已完全渲染,输出已准备好。
控制器之间的遍历
在我们的简化示例 1 的LittleNonsense
中,页面由单个顶层控制器驱动,您可能会觉得在当前控制器完成工作之前,遍历无法进行。这不是真的。在多个控制器的情况下,遍历可以随时超出当前控制器。遍历只是遍历渲染的作用域树,对于每个访问的作用域,系统在当前负责的控制器内部执行函数。例如,查看图 1.3 上的作用域树,并想象有一个嵌套控制器附加到ItemRepeater
作用域。如果发生这种情况,系统将不会在根控制器内部执行DataBind_ItemRepeater()
,而是在附加到ItemRepeater
的控制器内部执行此处理程序。此外,系统必须在调用任何绑定处理程序之前为此嵌套控制器运行模型准备阶段。完成后,遍历将返回到在根控制器上为重复轴调用DataBind_OrderRepeater()
。因此,执行可以超出当前控制器的边界,然后再次返回。
结论
看看您的控制器代码是多么清晰和直观!无论您的作用域树增长多大——控制器的渲染部分始终由一个简单的绑定处理程序列表组成。这意味着即使对于复杂的 Web 2.0 设计,您的代码的结构复杂性也不会增加!这反过来又带来了简单易读的源代码和精简的应用程序架构。至此,本教程的第一部分完成。我们已经学习了 DaST 设计和开发的基础知识,是时候深入真正的 Web 2.0 编程世界,看看 DaST 的全部功能了。
3. 高级 DaST 和 Web 2.0
在本教程的这一部分,我们将学习构建高度复杂和动态的 Web 2.0 站点所需的 DaST 其余功能。为了演示所有这些新功能,我设计了 LittleNonsense 演示的示例 2 部分。在本节的后面,我将详细解释此应用程序的源代码。
3.1. 示例 2 完整源代码
我将首先给出所有模板和控制器的完整源代码,这些模板和控制器参与示例 2 的实现。本节将用作本教程其余部分所有解释的代码参考。目前您可以快速浏览它。
首先,这次请求入口点是NonsenseExample2.aspx
页面。在NonsenseExample2.aspx.cs
文件中,一切都很简单,并且与我们在上一个示例中已经看到的类似(参见DaST 页面部分)。系统被告知使用NonsenseExample2Controller
来处理所有发送到此页面的请求。
现在,和以前一样,我将给出示例 2 的所有代码清单,并将在本文的其余部分逐行解释所有内容。让我们从包含根控制器模板的NonsenseExample2.htm
文件开始:
列表 3.1:../App_Data/Templates/NonsenseExample2.htm
01: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 02: 03: <html xmlns="http://www.w3.org/1999/xhtml"> 04: <head> 05: <title>ASP.NET DaST - Little Nonsense | {TodayDate}</title> 06: <link href="res/CSS/common.css" type="text/css" rel="stylesheet" /> 07: <script type="text/javascript" src="res/jQuery/jquery-1.7.1.min.js"></script> 08: </head> 09: <body class="some-class-{TodayDate}"> 10: 11: <div style="font-size: x-large; background-color: Gray; color: White; padding: 10px;">Little Nonsense | ASP.NET DaST Application DEMO</div> 12: <div style="margin: 0 10px;"><a href="NonsenseExample1.aspx">Example 1</a> | <a href="NonsenseExample2.aspx">Example 2</a></div> 13: <div style="margin: 20px; font-size: x-large; font-weight: bold;">Example 2: Partial Update, Actions, and Messages</div> 14: 15: <div dast:scope="CustomerRepeater" class="gizmo"> 16: <div dast:scope="Header" class="gizmo headerScope"></div> 17: <div dast:scope="Customer" class="gizmo"> 18: <div dast:scope="Header" class="gizmo headerScope"></div> 19: <div class="txt"><b>CUSTOMER:</b> [{CustomerID}] {CustomerName}</div> 20: <div dast:scope="OrderRepeater" class="gizmo"> 21: <div dast:scope="Header" class="gizmo headerScope"></div> 22: <div dast:scope="Order" class="gizmo"> 23: <div dast:scope="Header" class="gizmo headerScope"></div> 24: <div class="txt"><b>ORDER:</b> [{OrderID}] {OrderDate}</div> 25: <div dast:scope="ItemRepeater" class="gizmo"> 26: <div dast:scope="Header" class="gizmo headerScope"></div> 27: <div dast:scope="Item" class="gizmo"> 28: <div dast:scope="Header" class="gizmo headerScope"></div> 29: <div class="txt"><b>ITEM:</b> [{ItemID}] {ItemName}</div> 30: </div> 31: </div> 32: </div> 33: </div> 34: </div> 35: </div> 36: 37: <script language="javascript" type="text/javascript"> 38: 39: function markScopeUpdated(scopeID) 40: { 41: $("div[id='" + scopeID + "'].gizmo").addClass("updated bold") 42: .find(".gizmo").addClass("updated"); 43: } 44: 45: function hideRepeaterHeaders() 46: { 47: // leave only first gizmo header in case of repeater gizmos 48: $(".gizmo").each(function () { $(this).find("> .headerScope:not(:nth-child(1))").hide(); }); 49: } 50: 51: // add server message handler 52: DaST.Scopes.AddMessageHandler("{ScopeID}", "RefreshedFromServer", function (data) 53: { 54: markScopeUpdated(data.ScopeID); 55: hideRepeaterHeaders(); 56: }); 57: 58: // some initial load UI setup 59: $(document).ready(function () 60: { 61: $("form").attr("autocomplete", "off"); 62: hideRepeaterHeaders(); 63: }); 64: 65: </script> 66: </body> 67: </html>
下一个列表是用于显示所有作用域的头部部分的子控制器模板
列表 3.2:../App_Data/Templates/ScopeHeader.htm01: <div class="hdr"> 02: <span class="high-on-update"><b>Updated:</b> {Updated}</span> | 03: <b>ID:</b> {ScopeID}<br /> 04: <b>Pass data:</b> <input type="text" value="" /> 05: <a href="javascript:$.ScopeHeader('submit', '{ScopeID}', 'self')">Header Only</a> | 06: <a href="javascript:$.ScopeHeader('submit', '{ScopeID}', 'parent')">Parent Scope</a> | 07: <a href="javascript:$.ScopeHeader('submit', '{ScopeID}', 'child')">First Child Scope</a> | 08: <!--showfrom:data-posted--> 09: <b>Passed:</b> <span class="passedData">{PostedData}</span> 10: <!--showstop:data-posted--> 11: </div> 12: 13: <script language="javascript" type="text/javascript"> 14: 15: // since script is output for every header, 16: // avoid redefinition using if clause here 17: if (!$.ScopeHeader) 18: { 19: (function ($) 20: { 21: var methods = 22: { 23: submit: function (scopeID, target) 24: { 25: // clear all highlighted scope borders 26: $(".gizmo").removeClass("updated bold"); 27: 28: // collect data and raise action 29: var $scope = $("[id='" + scopeID + "']"); 30: var input = $scope.find("input:text").eq(0).val(); 31: if (typeof (input) == "string" && input.length > 0) 32: { 33: DaST.Scopes.Action(scopeID, 'RefreshFromClient', [input, target]); 34: } 35: else alert("You did not input any data for: " + scopeID); 36: } 37: }; 38: 39: $.ScopeHeader = function (options) 40: { 41: if (typeof (options) == "string" && methods[options]) 42: { 43: methods[options].apply(this, Array.prototype.slice.call(arguments, 1)); 44: } 45: }; 46: 47: })(jQuery); 48: } 49: 50: // add server message handler 51: DaST.Scopes.AddMessageHandler("{ScopeID}", "RefreshedFromServer", function (data) 52: { 53: markScopeUpdated("{ScopeID}"); 54: }); 55: 56: </script>
下一个列表是根控制器类本身
列表 3.3:../App_Code/Controllers/NonsenseExample2Controller.cs
001: public class NonsenseExample2Controller : ScopeController 002: { 003: public override string ProvideTemplate() 004: { 005: return Utils.LoadTemplate("NonsenseExample2.htm"); // load temlate content from anywhere 006: } 007: 008: public override void InitializeModel(ControllerModelBuilder model) 009: { 010: // scope binding handlers 011: model.SetDataBind(new DataBindHandler(DataBind_ROOT)); 012: model.Select("CustomerRepeater").SetDataBind(new DataBindHandler(DataBind_CustomerRepeater)); 013: model.Select("CustomerRepeater", "Customer").SetDataBind(new DataBindHandler(DataBind_Customer)); 014: model.Select("CustomerRepeater", "Customer", "OrderRepeater").SetDataBind(new DataBindHandler(DataBind_OrderRepeater)); 015: model.Select("CustomerRepeater", "Customer", "OrderRepeater", "Order").SetDataBind(new DataBindHandler(DataBind_Order)); 016: model.Select("CustomerRepeater", "Customer", "OrderRepeater", "Order", "ItemRepeater").SetDataBind(new DataBindHandler(DataBind_ItemRepeater)); 017: model.Select("CustomerRepeater", "Customer", "OrderRepeater", "Order", "ItemRepeater", "Item").SetDataBind(new DataBindHandler(DataBind_Item)); 018: 019: // set child controllers 020: model.Select("CustomerRepeater", "Header").SetController(new ScopeHeaderController()); 021: model.Select("CustomerRepeater", "Customer", "Header").SetController(new ScopeHeaderController()); 022: model.Select("CustomerRepeater", "Customer", "OrderRepeater", "Header").SetController(new ScopeHeaderController()); 023: model.Select("CustomerRepeater", "Customer", "OrderRepeater", "Order", "Header").SetController(new ScopeHeaderController()); 024: model.Select("CustomerRepeater", "Customer", "OrderRepeater", "Order", "ItemRepeater", "Header").SetController(new ScopeHeaderController()); 025: model.Select("CustomerRepeater", "Customer", "OrderRepeater", "Order", "ItemRepeater", "Item", "Header").SetController(new ScopeHeaderController()); 026: 027: // handle client actions 028: model.Select("CustomerRepeater", "Header").HandleAction("RaisedFromChild", new ActionHandler(Action_RaisedFromChild)); 029: model.Select("CustomerRepeater", "Customer", "Header").HandleAction("RaisedFromChild", new ActionHandler(Action_RaisedFromChild)); 030: model.Select("CustomerRepeater", "Customer", "OrderRepeater", "Header").HandleAction("RaisedFromChild", new ActionHandler(Action_RaisedFromChild)); 031: model.Select("CustomerRepeater", "Customer", "OrderRepeater", "Order", "Header").HandleAction("RaisedFromChild", new ActionHandler(Action_RaisedFromChild)); 032: model.Select("CustomerRepeater", "Customer", "OrderRepeater", "Order", "ItemRepeater", "Header").HandleAction("RaisedFromChild", new ActionHandler(Action_RaisedFromChild)); 033: model.Select("CustomerRepeater", "Customer", "OrderRepeater", "Order", "ItemRepeater", "Item", "Header").HandleAction("RaisedFromChild", new ActionHandler(Action_RaisedFromChild)); 034: } 035: 036: // 037: #region Action Handlers 038: 039: private void Action_RaisedFromChild(object arg) 040: { 041: if ((string)arg == "parent") 042: { 043: CurrPath(-1).Refresh(); // refresh parent scope of the root scope of header controller 044: CtrlPath().MessageClient("RefreshedFromServer", new { ScopeID = CurrPath(-1).ClientID }); // notify client of refreshed scope 045: } 046: else if ((string)arg == "child") 047: { 048: // find name of the next scope depending on current scope 049: string nextScope = null; 050: string currScope = CurrPath(-1).ClientID.Split('-').Last(); 051: switch (currScope) 052: { 053: case "CustomerRepeater": nextScope = "Customer"; break; 054: case "Customer": nextScope = "OrderRepeater"; break; 055: case "OrderRepeater": nextScope = "Order"; break; 056: case "Order": nextScope = "ItemRepeater"; break; 057: case "ItemRepeater": nextScope = "Item"; break; 058: } 059: // invoke action on Header scope of the first child scope 060: if (nextScope != null) 061: { 062: CurrPath(-1, nextScope, "Header").InvokeAction("InvokedFromParent", null); 063: } 064: } 065: } 066: 067: #endregion 068: 069: // 070: #region Binding Handlers 071: 072: private void DataBind_ROOT() 073: { 074: CurrPath().Replace("{TodayDate}", DateTime.Now.ToString("yyyy-MM-dd")); // output some example values 075: CurrPath().Replace("{ScopeID}", CurrPath().ClientID); // bind scope client id 076: } 077: 078: private void DataBind_CustomerRepeater() 079: { 080: var customers = DataLayer.GetCustomers(); // get all customers 081: CurrPath().RepeatStart(); 082: foreach (var customer in customers) // repeat scope for each customer 083: { 084: CurrPath().Repeat(); 085: CurrPath("Customer").StoredParams.Set("CustomerID", customer.GetValue("ID")); // pass customer ID to each child Customer scope 086: } 087: } 088: 089: private void DataBind_Customer() 090: { 091: var customerID = CurrPath().StoredParams.Get<string>("CustomerID"); // get customer ID set in previous binding handler 092: var customer = DataLayer.GetCustomer(customerID); // get customer object by ID 093: CurrPath().Replace("{CustomerID}", customer.GetValue("ID")); 094: CurrPath().Replace("{CustomerName}", customer.GetValue("Name")); 095: } 096: 097: private void DataBind_OrderRepeater() 098: { 099: var customerID = CurrPath(-1).StoredParams.Get<string>("CustomerID"); // get customer ID from parent Customer scope 100: var orders = DataLayer.GetOrders(customerID); // get orders by customer ID 101: CurrPath().RepeatStart(); 102: foreach (var order in orders) // repeat scope for each order 103: { 104: CurrPath().Repeat(); 105: CurrPath("Order").StoredParams.Set("OrderID", order.GetValue("ID")); // pass order ID to each child Order scope 106: } 107: } 108: 109: private void DataBind_Order() 110: { 111: var orderID = CurrPath().StoredParams.Get<string>("OrderID"); // get order ID set in previous binding handler 112: var order = DataLayer.GetOrder(orderID); // get order object by ID 113: CurrPath().Replace("{OrderID}", order.GetValue("ID")); 114: CurrPath().Replace("{OrderDate}", order.GetValue("Date")); 115: } 116: 117: private void DataBind_ItemRepeater() 118: { 119: var orderID = CurrPath(-1).StoredParams.Get<string>("OrderID"); // get order ID from parent Order scope 120: var items = DataLayer.GetOrderItems(orderID); // get order items by order ID 121: CurrPath().RepeatStart(); 122: foreach (var item in items) // repeat scope for each item 123: { 124: CurrPath().Repeat(); 125: CurrPath("Item").StoredParams.Set("ItemID", item.GetValue("ID")); // pass item ID to each child Item scope 126: } 127: } 128: 129: private void DataBind_Item() 130: { 131: var itemID = CurrPath().StoredParams.Get<string>("ItemID"); // get item ID set in previous binding handler 132: var item = DataLayer.GetOrderItem(itemID); // get item object by ID 133: CurrPath().Replace("{ItemID}", item.GetValue("ID")); 134: CurrPath().Replace("{ItemName}", item.GetValue("Name")); 135: } 136: 137: #endregion 138: }
最后一个列表是用于所有作用域头部的子控制器
列表 3.4:../App_Code/Controllers/ScopeHeaderController.cs01: public class ScopeHeaderController : ScopeController 02: { 03: public override string ProvideTemplate() 04: { 05: return Utils.LoadTemplate("ScopeHeader.htm"); // load template content from anywhere 06: } 07: 08: public override void InitializeModel(ControllerModelBuilder model) 09: { 10: // scope binding handlers 11: model.SetDataBind(new DataBindHandler(DataBind_ROOT)); 12: 13: // handle client actions 14: model.HandleAction("RefreshFromClient", new ActionHandler(Action_RefreshFromClient)); 15: model.HandleAction("InvokedFromParent", new ActionHandler(Action_InvokedFromParent)); 16: } 17: 18: // 19: #region Action Handlers 20: 21: private void Action_RefreshFromClient(object arg) 22: { 23: // client call was DaST.Scopes.Action(scopeID, 'RefreshFromClient', [input, target]); 24: string input = (string)((object[])arg)[0]; // 1st elem of passed JSON array 25: string target = (string)((object[])arg)[1]; // 2nd elem of passed JSON array 26: 27: switch (target) 28: { 29: case "self": break; 30: // notify parent scope by rasing action passing string target as parameter 31: case "parent": CtrlPath().RaiseAction("RaisedFromChild", "parent"); break; 32: case "child": CtrlPath().RaiseAction("RaisedFromChild", "child"); break; 33: default: throw new Exception("Unknown target"); 34: } 35: 36: CtrlPath().Params.Set("PostedData", input); // set posted data parameter 37: 38: CtrlPath().Refresh(); // refresh control root scope 39: CtrlPath().MessageClient("RefreshedFromServer", null); // notify client of refreshed scope 40: } 41: 42: private void Action_InvokedFromParent(object arg) 43: { 44: CtrlPath().Refresh(); // refresh control root scope 45: CtrlPath().MessageClient("RefreshedFromServer", null); // notify client of refreshed scope 46: } 47: 48: #endregion 49: 50: // 51: #region Binding Handlers 52: 53: private void DataBind_ROOT() 54: { 55: CurrPath().Replace("{Updated}", DateTime.Now.ToString("HH:mm:ss")); // bind update time 56: CurrPath().Replace("{ScopeID}", CurrPath().ClientID); // bind scope client id 57: if (CurrPath().Params.Has("PostedData")) // if PostedData param is set for the scope ... 58: { 59: var data = CurrPath().Params.Get<string>("PostedData"); // retrive posted data containing user input 60: 61: CurrPath().Replace("{PostedData}", data); // bind input value to output it 62: CurrPath().AreaConditional("data-posted", true); // display data-posted conditional area 63: } 64: else 65: { 66: CurrPath().AreaConditional("data-posted", false); // hide data-posted conditional area 67: } 68: } 69: 70: #endregion 71: }
如您所见,代码中有很多有趣的东西。下面我将快速介绍示例 2 演示,然后我将讨论嵌套控制器和绑定处理程序中的新功能,最后,我将转向最激动人心的部分,即动作、AJAX 部分更新和双工消息。
3.2. 示例 2 简介
我设计了示例 2 应用程序,以演示您可能需要创建复杂 Web 2.0 设计的所有 DaST 编程技术:AJAX 部分更新、多个嵌套控制器、事件处理、双工消息等。同时,此演示非常简单明了。您可能已经猜到,DaST 部分更新机制与 DaST 世界中的其他所有内容一样,都基于作用域树。简单来说,树中的每个作用域都可以独立刷新。示例 2 的主要思想是在结果页面上可视化渲染作用域树的物理作用域容器,并允许对随机作用域进行部分更新,以便您实际了解作用域树的工作原理、作用域如何更新、数据如何传递、控制器如何通信、感受工作流程以及感受 DaST 模式的真正强大之处。
新的作用域树
在示例 1 中,我们的初始作用域树由 3 个中继器作用域组成(参见作用域树部分)。这种结构允许我们更新整个中继器,但不能更新单个重复项。在示例 2 中,我们将重复项包装到单独的作用域中,以便部分更新也可以应用于这些单个项。因此,除了CustomerRepeater
、OrderRepeater
和ItemRepeater
作用域之外,我们的树还获得了Customer
、Order
和Item
作用域。接下来,为了更好地可视化作用域,我想为树中的每个作用域输出一个特殊的信息头部。此头部将显示在每个作用域容器的顶部,并显示作用域客户端 ID、上次更新时间戳和链接按钮,以触发动作并执行部分更新。我想将头部封装在一个单独的 Header 作用域中,该作用域将是树中所有其他作用域的第一个子作用域。示例 2 的结果初始作用域树如下所示
图 3.1:LittleNonsense 演示中 NonsenseExample2.aspx 页面的初始作用域树
有了这种作用域树结构,我就可以控制页面的所有可能部分。我可以应用部分更新或将数据传递到任何我想要的任何作用域组合中。现在让我们看看此演示的 UI 输出。
UI 概述
此演示的一部分 UI 输出如下所示,见图 EX2
图 EX2:示例 2 输出的一部分
如您所见,它本质上与示例 1 中的分层结构相同,都基于相同的客户订单 XML 数据文件,但功能更丰富,UI 更复杂。我用于可视化作用域的主要方法是在每个作用域容器周围添加一个虚线边框。现在我们可以实际看到作用域,并确保它们的嵌套配置严格对应于图 3.1 中的初始作用域树。此外,由于上次回发而部分更新的作用域周围的边框将是红色。直接更新的作用域将具有粗红色边框,而由于父作用域更新而更新的作用域将具有常规红色边框。您很快就会看到这是什么样子。其次,来自 XML 输入文件的所有实际数据值输出都标记为蓝色背景。这样做只是为了将其与页面上与 XML 文件中的值无关的其他标记进行视觉区分。最后,每个作用域都有一个黄色背景突出显示的头部。正如我之前所说,头部部分被包装在自己的Header
作用域中,因此您也可以看到头部周围的虚线边框。现在让我们仔细看看头部。
头部控制器
信息头部不仅有自己的作用域,还有自己的控制器!通过这个例子,我演示了如何使用多个控制器以及所有相关的技术和 API。如果我们在示例 1 中尝试使用单个控制器和单个模板(像我们以前那样)来实现图 3.1 中的作用域树,我们最终会在模板和后端控制器中重复头部节代码。将头部节分解到带有部分模板的独立控制器中,并在每次作用域需要头部节时重复使用它,这是非常直观的。
头部 UI 输出
首先,头部包含有关当前作用域的一些基本信息。您可以看到“更新时间”时间戳值,该值在作用域部分更新时会发生变化。这有助于您区分部分更新的作用域和未更新的作用域。下一个值是“ID”,其中包含当前Header
作用域的客户端 ID。回想一下,作用域 ID 是使用作用域路径构建的,该路径对于每个作用域都是唯一的。在头部显示此值有助于理解作用域路径是如何构建的,以及它们如何与分层嵌套结构相对应。请注意,显示的 ID 始终是当前Header
作用域的。要查找父作用域的 ID,我们只需从原始路径中删除最后一个段,例如,如果SCOPE$0-CustomerRepeater$0-Header
指向Header
作用域,则SCOPE$0-CustomerRepeater
指向父CustomerRepeater
作用域,依此类推。头部动作链接
作用域头部的最有趣部分是输入框和 3 个链接按钮。这些是演示数据输入和所有 Web 2.0 相关机制所需的。这个想法是您在输入字段中输入随机文本,然后单击左侧的其中一个链接。这将触发客户端动作,该动作将您的输入数据传递到服务器,处理它,将部分更新应用于特定作用域,并启动双工回复消息,该消息在客户端处理。整个过程将在本教程的其余部分详细解释。现在我只描述这 3 个链接按钮的功能。因此,让我们选择作用域头部并在其中输入“Hello World!!!”。
现在单击第一个“HeaderOnly”链接。结果将如下
这里您看到头部周围的粗红色边框,表示作用域已直接更新。当我们单击链接按钮时,动作被触发,我们的输入文本连同动作一起传递到服务器端。在服务器端,我们指示系统在作用域内显示此文本,并对该作用域应用部分更新以反映更改。这是所有 Web 2.0 应用程序的典型交互工作流。结果是,您在右侧的“Passed”字段中看到您的输入文本输出,并且作用域被高亮显示。因此,“Header Only”链接表示传递文本并仅更新头部。现在在同一字段中输入“Hello World 2!!!”并单击第二个链接“Parent Scope”。结果如下
这次头部和父作用域都直接更新了。直接更新意味着我们将明确要求系统更新这些作用域。所有子作用域也作为父作用域更新的结果而更新,因此它们具有常规的红色边框。更新父作用域的意义在于向您展示嵌套控制器如何使用控制器动作进行通信。由于头部有自己的控制器,并且ItemRepeater
由根控制器负责,因此为了更新ItemRepeater
,我们将不得不通知根控制器。我将在后续部分深入解释这个有趣的过程。最后,输入“Hello World 3!!!”并单击第三个链接“First Child Scope”。您将看到以下内容
现在我们看到当前作用域的头部以及第一个子作用域的头部都更新了。这是更复杂的控制器通信,我们需要首先通知根控制器,然后根控制器需要访问当前作用域的第一个子节点并通知其头部。所以这需要两个步骤:从子控制器到父控制器,以及从父控制器到另一个子控制器。现在是时候深入研究代码,看看这一切是如何实际工作的了。我们将从遍历控制器渲染和绑定处理程序部分开始,这在前面的示例中我们已经熟悉了。
3.3. 控制器设置和绑定处理程序
在继续阅读之前,请回顾控制器部分的所有理论。
在本节中,我们将介绍示例 2 控制器绑定处理程序以及控制器设置的源代码。从前面示例中的控制器类部分,我们已经知道绑定处理程序的工作原理和用途。但这次示例应用程序的功能更丰富,并且在渲染过程中有一些新的功能需要强调。当然,主要区别在于我们现在有多个带有多个模板的嵌套控制器,因此我们想看看在这种情况下一切是如何工作的。
一般信息
在示例 2 中,我们使用两个不同的控制器,每个控制器都有独立的模板。根控制器是列表 3.3 上的NonsenseExample2Controller
,其模板在列表 3.1 的NonsenseExample2.htm
中。另一个控制器是列表 3.4 上的ScopeHeaderController
,其模板在列表 3.2 的ScopeHeader.htm
中。此控制器用于在页面上显示每个作用域的头部部分。
现在查看图 3.1 上的作用域树。我们需要将子ScopeHeaderController
附加到树中的每个Header
作用域,使其负责渲染头部部分。附加子控制器是模型准备渲染阶段的一部分(回顾控制器渲染部分)。从 HTML 模板的角度来看,您要附加控制器的作用域的容器元素必须为空,即不包含任何标记。这是很自然的,因为每个子控制器都有自己的模板,其渲染结果在渲染遍历期间会填充到该空容器中。
渲染遍历不受子控制器存在的影响。它仍然以相同的遍历顺序遍历作用域树,在适当的控制器内部执行绑定处理程序。因此,对于当前遍历的数据作用域,系统会找到负责的控制器,并尝试在其内部执行相应的绑定处理程序。回顾上面控制器之间的遍历部分,我提到了执行如何在父控制器和子控制器之间来回切换。
现在是时候从 HTML 模板开始接触一些源代码了。
多个模板
我们的根模板位于列表 3.1。它负责除了头部部分之外的所有 UI。根模板总是看起来像一个完整的网页,包含<HTML>
和<BODY>
标签,而子模板只包含部分 HTML 片段。
定义数据作用域树的所有作用域容器位于第 15-35 行。嵌套结构与我们之前的示例类似,但这次我们有更多的作用域。多个Header
作用域(第 16、18、21、23、26 和 28 行)将附加ScopeHeaderController
。在模板中,这些作用域如下所示
16: <div dast:scope="Header" class="gizmo headerScope"></div>
所以,正如我已经说过的,如果我们要将控制器附加到作用域,它的容器必须是空的;否则,会抛出错误。子控制器有自己的模板,在渲染后填充到这个空容器中。
在第 37-65 行还有一个 JavaScript 块。除了第 52-56 行,其中我使用 DaST 最酷的功能之一来处理从服务器端发送的消息外,大部分 JavaScript 只是为了更精美的输出。结合客户端动作,这种机制被称为双工消息传递,我将在接下来的章节中深入讨论所有这些。
现在查看列表 3.2 中的子模板。在第 1-11 行,它有一些标记来输出头部 UI。请注意,这里没有嵌套作用域,因此子ScopeHeaderController
负责的作用域子树是单个Header
作用域,它也是该控制器同时的根作用域。标记很简单。有一些占位符用于显示更新时间戳和作用域 ID,以及在第 5-7 行用于触发动作的链接按钮。动作将在动作部分深入解释。在第 8-10 行,我们使用另一个 DaST 功能,称为条件区域。我将在接下来的章节中解释此功能。子模板还在第 13-56 行包含一个 JavaScript 块。我使用典型的 jQuery 语法创建$.ScopeHeader
插件来准备数据和触发动作。所有这些都将在动作部分解释。请注意第 17 行的if
条件 – 它用于避免插件类重新定义,因为结果页面上有多个Header
作用域,因此此脚本将被粘贴多次。我可以直接将其放在父模板中,问题就会消失,但我只是想展示如何编写包含标记和脚本混合的完全独立的模板。
设置子控制器
现在让我们转向控制器源代码。查看图 3.1 中的新作用域树。在根控制器中,我们需要做的第一件事是将子ScopeHeaderController
附加到所有Header
作用域,使其负责渲染头部部分。子控制器应该在父控制器的InitializeModel()
[*]重写中使用SetController()
[*] API 附加,其中传递所需子控制器的实例。
因此,在列表 3.3 中顶层控制器的第 20-25 行,我们将ScopeHeaderController
附加到作用域树中的每个Header
作用域。InitializeModel()
[*]中的每个调用都遵循相同的思想——使用Select()
[*]选择目标作用域,然后对其调用所需的设置 API。因此,附加子控制器的典型调用如下所示:
20: model.Select("CustomerRepeater", "Header").SetController(new ScopeHeaderController());
SetController()
[*]需要将控制器类的实例作为参数传递。我们可以到处传递相同的实例,或者像我一样每次都使用新的实例——这无关紧要,因为根据设计,我们在控制器内部无论如何都不依赖实例,而是使用上下文相关的设施,例如Params
[*]集合等。
现在,控制器附加后,我们的作用域树已准备好进行遍历。将所有与动作相关的内容留到以后的部分,让我们快速回顾一下两个控制器中的绑定处理程序。从控制器类部分,我们已经知道绑定处理程序的工作原理和用途。因此,让我们从列表 3.3 中的根控制器开始。
根控制器绑定处理程序
在列表 3.3 的第 11-17 行,我们做了一些样板代码,使用SetDataBind()
[*]将绑定处理程序与树中的作用域关联起来。这里我们有比示例 1 中以前的控制器更多的作用域绑定表达式,仅仅因为这次有更多的作用域。我们绑定了图 3.1 作用域树中除了Header
作用域之外的所有作用域,因为此作用域有一个附加的子控制器,该控制器负责渲染此作用域。
绑定处理程序实现位于第 72-135 行。在前面的示例中,我们只有 3 个嵌套中继器的处理程序。现在我们必须为Customer
、Order
和Item
作用域添加更多处理程序,这些作用域将单个项包装在相应的中继器内部。除此之外,思路大致相同:检索项,循环重复作用域,将当前项保存在参数中等。让我们快速浏览一些绑定处理程序。
第一个绑定处理程序是第 72 行的DataBind_ROOT()
,用于控制器根作用域,它也是一个NULL
作用域。没什么特别的。只需替换根作用域中的一些占位符。
下一个是第 78 行的DataBind_CustomerRepeater()
。我们已经很熟悉了:我们获取客户对象列表,循环遍历它们,并重复当前作用域。与之前的示例不同,我们不需要在这里替换任何占位符,因为此中继器中的每个项都有一个专用的Customer
作用域,其绑定处理程序应该完成所有替换。我们唯一需要的是使用作用域参数将客户对象传递给下一个遍历的作用域。
这里有一个有趣的时刻。在第 85 行,为了将客户传递给下一个作用域,我使用StoredParams
[*]集合而不是Params
[*]
85: CurrPath("Customer").StoredParams.Set("CustomerID", customer.GetValue("ID"));
这次我传递的是可以直接用于检索该客户的 ID,而不是完整的客户对象。为什么我不能使用Params
[*]来完成此操作?Params
[*]仅在我们需要在同一遍历运行中执行的处理程序之间传递数据时才有效。但是,由于在示例 2 中,每个作用域都可以随机刷新,导致遍历从该作用域重新执行绑定处理程序,我不能再依赖Params
[*]——除非在当前遍历运行中执行DataBind_CustomerRepeater()
,否则客户 ID 将不存在。使用StoredParams
[*]代替可以轻松解决问题,因为保存在StoredParams
[*]中的值在后续动作和回发期间会持久存在。
我建议不要大量使用StoredParams
[*],因为它的理念类似于标准 ASP.NET 的VIEWSTATE
,它会在 Ajax 请求之间增加一些开销。所有你可以用StoredParams
[*]做的事情,你也可以不用它,只需通过动作传递参数即可。对于我们的示例,我只是想演示如果你真的需要如何使用StoredParams
[*]。
下一个处理程序是第 89 行的DataBind_Customer()
。其实现很简单。在第 91 行,我们检索之前保存的客户 ID。即使遍历从当前绑定处理程序(部分更新)开始,StoredParams
[*]仍将包含在初始页面加载时(当遍历整个树时)保存的所需客户 ID 值。在第 92 行,我们使用客户 ID 获取客户对象。在第 93-94 行,我们替换占位符。
下一个是第 99 行的DataBind_OrderRepeater()
。在这里我们看到了一个有趣的现象——作用域路径中的负数
99: var customerID = CurrPath(-1).StoredParams.Get<string>("CustomerID");
阅读CurrPath
[*] API 描述,并参考图 3.1 中的示例 2 作用域树。当前作用域是OrderRepeater
,负数-1
告诉系统向后移动 1 个作用域,即指向前一个Customer
作用域。由于客户 ID 是与Customer
作用域一起保存的,因此我们始终可以通过指向此客户作用域来检索此 ID。
此处理程序中的其他一切都与之前的重复器相同。我们通过客户 ID 检索订单列表,在循环中重复作用域,并在每次迭代中将当前订单 ID 传递给Order
作用域。
等等。NonsenseExample2Controller
中的其他绑定处理程序也是类似的。
子控制器绑定处理程序
现在让我们看看列表 3.4 中的子ScopeHeaderController
中发生了什么。此控制器附加到的Header
作用域成为此控制器的根作用域。除了根作用域,此控制器中没有更多作用域。因此,我们只需要一个用于根作用域的绑定处理程序。
处理程序设置在第 11 行,回调函数DataBind_ROOT()
在第 53 行。此绑定处理程序将渲染整个头部 UI。首先,在第 55 行和第 56 行,我们将{Updated}
和{ScopeID}
占位符分别替换为当前时间戳和当前作用域的ClientID
[*]。查看列表 3.2 中的模板,其中插入了所有这些值。接下来,在第 57 行,我们使用Params.Has()
[*]检查是否设置了名为"PostedData"
的参数。如果是,则此数据输出到{PostedData}
占位符;否则,我们隐藏整个输出区域。在初始加载时,"PostedData"
参数未设置。在随后的加载中,它由动作处理程序为当前作用域设置,以响应用户在作用域头部内的文本框中输入。这将在我们的下一个动作部分深入解释。目前,绑定处理程序中需要解释的最后一件事是第 62 行和第 66 行的AreaConditional()
[*] API 的用法。
条件区域
快速查看一下AreaConditional()
[*] API。这只是一种根据某些条件隐藏或显示 HTML 代码片段的方法。此片段必须包装在一个具有特殊语法的特殊注释标签中。在列表 3.2 中的子控制器模板中,我们在第 8-10 行有这样一个条件
08: <!--showfrom:data-posted--> 09: <b>Passed:</b> <span class="passedData">{PostedData}</span> 10: <!--showstop:data-posted-->
该区域通过绑定处理程序使用AreaConditional()
[*] API 进行控制。在列表 3.4 的第 66 行,我们有以下语句:
66: CurrPath().AreaConditional("data-posted", false);
这告诉系统从结果页面中删除指定区域。在第 62 行,我们为第二个参数传递TRUE
,这意味着该区域需要添加到结果页面。我们也可以通过将条件区域包装在作用域中并在必要时隐藏它来实现相同的效果,但我们不想用不必要的作用域来淹没我们的作用域树。条件区域是页面上切换不同 UI 区域的轻量级方法。
区域条件可以嵌套以实现逻辑AND,或并排放置以实现逻辑OR。您可以以多种不同的配置组合条件,以满足您的 UI 需求。关于条件需要记住的一点是,它们不应包含嵌套作用域。这不会破坏任何东西,但从设计的角度来看,使用父条件区域隐藏作用域是没有意义的。在这种情况下,应使用父作用域来维护清晰的作用域树。
3.4. 作用域动作
动作和双工消息机制与部分更新相结合,是使 DaST 与其他框架不同的另一个突出特性。DaST 概念的主要优势在于核心功能设计的简单性和灵活性同时达到最高水平,而其他现代服务器页面引擎通常不得不牺牲其中一个来换取另一个。动作机制是 DaST 替代事件处理的方案。典型的动作工作流程如下:
- 用户与网页进行交互(点击按钮等)。
- 客户端脚本触发一个带可选数据值的动作。
- 包含动作信息的请求发送到服务器端。
- 带数据值参数的动作处理程序在控制器内部执行。
- 作用域被刷新(如果需要),并且动作以部分更新完成。
在我们的示例 2 中,当用户在每个作用域头部区域的文本框中输入一些文本并点击右侧的链接按钮之一时,会触发动作。在简介部分,我已经描述了这 3 个链接按钮的区别以及由此产生的动作顶层工作流。让我们从头开始遍历此过程并详细解释所有内容。
触发动作
动作在客户端浏览器中使用DaST.Scopes.Action()
[*] API 触发。传递给此函数的scopePath
是负责处理事件的作用域的 ID。actionName
参数标识控制器内的动作。最后,actionArgs
是随动作传递的通用数据参数。DaST.Scopes.Action()
[*]函数可以从网页中的任何位置调用。您可以将其放入 HTML 元素的onclick
属性中,或从 JavaScript 调用它——没有限制。现在回到我们列表 3.2 的示例。假设我们输入“Hello World!!!”并点击了“Header Only”链接按钮,如示例 2 简介部分所述。此动作的结果也已在示例 2 简介部分中描述。现在让我们跟随代码,看看发生了什么以及它究竟是如何工作的。“Header Only”链接按钮位于列表 3.2 的第 5 行,它看起来像这样
5: <a href="javascript:$.ScopeHeader('submit', '{ScopeID}', 'self')">Header Only</a>
所有 3 个按钮都调用$.ScopeHeader
插件的相同submit
方法(我使用 jQuery 插件语法只是因为我喜欢它),并根据点击的按钮传递不同的参数。该方法在第 23-36 行实现。尽管它与动作没有直接关系,我还是在这里稍作解释。submit
方法的目的是从相应的文本框中获取输入文本,准备一些值,并触发动作,将文本和准备好的值作为参数传递。由于点击了“Header Only”,target
参数等于“self”。在第 26 行,我清除了所有加粗和红色边框,将它们恢复到 Ajax 调用之前的初始状态。在第 29 行,我使用传递的scopeID
值来查找点击按钮的当前作用域容器。请注意,jQuery 不会接受带有“$”分隔符的 DaST ID 规范,因此我必须使用属性选择器。我将来会更改分隔符来解决这个问题。接下来,在第 30 行,我获取当前输入文本。接下来,我们在第 31 行有一个if
子句,并在第 35 行显示警告消息,以防输入无效。最后,如果输入有效,我们来到第 33 行,所有有趣的事情都在这里发生
33: DaST.Scopes.Action(scopeID, 'RefreshFromClient', [input, target]);
这是我们期待已久的DaST.Scopes.Action()
[*] API 调用,用于触发客户端动作。scopeID
参数在这里被重用,以指向其负责的控制器应处理动作的作用域。回想一下,我们当前在其中一个Header
作用域的模板中,因此scopeID
将根据我们点击按钮的位置指向其中一个Header
作用域。附加到此作用域的控制器是ScopeHeaderController
,因此系统将使用其实例来处理当前动作,这是期望的行为。第二个参数是动作名称,我选择了"RefreshFromClient"
。它不言自明,表示“这是来自客户端的刷新请求”。请注意,动作名称在整个控制器中必须是唯一的,而不是针对目标作用域。最后一个参数是通用数据值,将传递给服务器端的动作处理程序。此值必须是可 JSON 序列化的对象。在本例中,我们传递一个 JSON 数组,其中包含输入文本和目标(在本例中,其值为"self"
)。现在动作已触发。让我们看看如何在服务器端处理此动作。
处理动作
在控制器类中,动作通过HandleAction()
[*] API 在InitializeModel()
[*] 方法内部绑定到它们的处理程序。无需使用Select()
[*] 指向特定作用域,因为客户端动作是按控制器绑定的。动作处理程序函数必须具有一个通用对象参数,该参数等于传递给客户端DaST.Scopes.Action()
[*] 的数据值。在动作处理程序内部,CurrPath()
[*] 始终指向由DaST.Scopes.Action()
[*] 调用中的scopePath
参数指定的作用域。回到我们的示例。正如我们从上一节中了解到的,系统选择附加到Header
作用域的ScopeHeaderController
来处理我们的"RefreshFromClient"
动作。此控制器的代码在列表 3.4 中。我们首先要做的是将动作绑定到其处理程序,这在第 14 行完成,如下所示
14: model.HandleAction("RefreshFromClient", new ActionHandler(Action_RefreshFromClient));
因此,您传递动作名称和处理程序函数。Action_RefreshFromClient()
处理程序的实现在第 21-40 行。让我们遍历此处理程序的源代码并解释它的作用。此处理程序的目的是获取用户输入的文本,将其保存在提交此数据的作用域的"PostedData"
参数中,然后刷新某些作用域以使其重新渲染。由于我们使用了“Header Only”按钮,我们只需要刷新当前的Handler
作用域(目标等于"self"
)。从子控制器绑定部分,我们已经知道第 53-68 行的DataBind_ROOT()
绑定处理程序是如何工作的。在重新渲染时,此处理程序会再次被调用。这次"PostedData"
"
值已设置,条件区域显示带有实际输入数据的 HTML 片段,因此您在红色矩形中看到“Hello World!!!”输出(参见头部动作链接部分)。
首先,处理函数有一个通用arg
参数,该参数设置为从客户端传递给DaST.Scopes.Action()
[*]的 JSON 值。JSON 对象在 .NET 中表示为数组和名称-值字典的组合,因此使用它们非常简单。回想一下,我传递了一个包含两个值的数组:输入文本和目标。在第 24 行和第 25 行,我将这些值检索到局部变量中。接下来,在第 27 行,我们根据目标值进行切换,在本例中什么也不做,因为目标等于"self"
。所有其他情况也将在后续教程章节中解释。CurrPath()
[*]指向控制器根目录,即Header
作用域。在第 36 行,我们将“Hello World!!!”文本以"PostedData"
名称保存到当前Header
作用域,以便在重新渲染时在DataBind_ROOT()
中获取。最后,我们在动作处理程序的末尾注意到两个更有趣的表达式。在第 38 行,我们看到 DaST 部分更新用于刷新当前Header
作用域。第 39 行演示了 DaST 双工消息机制,用于通知客户端事件并向其传递数据。这两个很酷的功能将在接下来的两节中深入解释。
3.5. 部分更新
在 DaST 中,部分更新可以应用于树中的任何作用域或多个作用域。如果作用域被刷新,其子作用域也会被刷新。除非明确指示,否则 DaST 不会在回发时刷新任何作用域。没有像UpdatePanel
触发器或标准 ASP.NET 中的类似内容。要在 DaST 中刷新作用域,您需要使用CtrlPath()
[*] 或CurrPath()
[*]指向它,然后调用Refresh()
[*]函数。如果您对多个作用域执行此操作,它们都会被刷新。就这么简单。当对指向的作用域调用Refresh()
[*]时,此作用域会放入刷新队列中。在阶段 2:动作处理完成后,阶段 3:渲染输出开始(参见控制器渲染部分)。这次,不是从NULL
作用域遍历作用域树,而是从刷新队列中的作用域开始部分遍历树。每个子树的最终输出会发送回客户端,并应用于网页上相应的作用域。重要的是要理解,在部分更新期间,DaST 仅执行刷新作用域的绑定处理程序!这是一种数学上正确的局部更新方法,当局部 UI 刷新导致后端代码局部执行时。与标准 ASP.NET 中基于UpdatePanel
的丑陋更新相比,每次局部更新都会执行整个页面生命周期。现在回到我们列表 3.4 中的示例代码。刷新通过第 38 行的以下指令完成
38: CtrlPath().Refresh();
“Header Only”链接只刷新一个作用域,即与点击链接按钮的头部对应的Header
作用域。由于此指令,第 53 行的DataBind_ROOT()
将在渲染遍历期间重新执行,新生成的输出将应用于网页上相应的作用域。点击“Header Only”链接的结果 UI 已在头部动作链接部分中显示。为了用红色边框突出显示特定作用域,我需要在客户端检测此作用域是否已刷新。为此,我可以使用我最喜欢的 DaST 功能之一——双工消息。
3.6. 双工消息
简单来说,客户端消息机制就像动作一样,但方向相反。动作在客户端触发并在服务器端处理,而消息在服务器端触发并由客户端浏览器中的 JavaScript 处理!动作和客户端消息相辅相成,允许客户端和服务器之间进行真正的双向双工通信。这种机制是 DaST 真正值得骄傲的,因为凭借其所有简单性,它为您的应用程序带来了无与伦比的 Web 2.0 功能。语法像 DaST 中的所有其他内容一样统一:要向客户端浏览器发送消息,您需要指向作用域并对其调用MessageClient()
[*]函数,传递消息 ID 和 JSON 数据对象。在客户端脚本中,我们使用DaST.Scopes.AddMessageHandler()
[*] JavaScript API 为特定作用域和消息 ID 组合添加处理程序。回到我们列表 3.4 中的代码。在我们的动作处理程序的最后第 39 行,我们看到以下指令
39: CtrlPath().MessageClient("RefreshedFromServer", null);
这里我向客户端发送"RefreshedFromServer"
消息,以便客户端代码可以处理此消息并相应地更新所需的作用域边框。MessageClient()
[*]的第二个参数是一些通用数据值,必须是可 JSON 序列化的对象,如前所述。此对象将传递给客户端并作为参数传递给 JavaScript 消息处理程序。在本例中,我们不需要传递任何数据,所以只需传递null
。现在让我们看看消息如何在客户端处理。在列表 3.2 的第 51-54 行,我们看到以下内容
51: DaST.Scopes.AddMessageHandler("{ScopeID}", "RefreshedFromServer", function (data) 52: { 53: markScopeUpdated("{ScopeID}"); 54: });
这就是我们消息的 JavaScript 处理程序!{ScopeID}
被实际值替换,指向当前Header
作用域。回想一下,在我们的动作处理程序内部,"RefreshedFromServer"
消息也发送给了Header
作用域。DaST.Scopes.AddMessageHandler()
[*]的第三个参数是一个回调函数,带有data
参数,该参数获取传递给MessageClient()
[*]的 JSON 表示值。我喜欢 JSON,因为它到处都是一致的。请注意,此代码不是从第 17 行开始的if(!$.ScopeHeader)
条件的一部分。此条件需要避免$.ScopeHeader
插件重新定义,因为它对于所有Header
作用域始终相同。但第 51-54 行的代码由于{ScopeID}
的不同值而始终不同,在我们的例子中,需要为页面上的每个Header
作用域添加消息处理程序。接下来,在第 53 行的回调函数内部,我调用了一个辅助函数markScopeUpdated()
来赋予我们的作用域更新的外观和感觉。此辅助函数在列表 3.1 的根模板的第 39-43 行实现。您可以看到我只是选择更新的作用域容器并为其分配 CSS 类,使其变为红色和粗体。我还将所有子容器变为红色,但不加粗,以便只有直接更新的作用域变为粗体。这实现了我们在示例 2 UI部分中看到的 UI。就这样!我们的作用域得到更新,消息得到处理,UI 得到调整。现在是时候看看另外两个链接按钮的作用了。
3.7. 控制器通信
我们需要学习的最后一个主题是控制器到控制器通信。在 DaST 中,控制器使用控制器动作相互通信。DaST 最大的优点之一是控制器动作的语法与客户端触发的动作完全一致。假设我们在ItemRepeater
的头部输入了“Hello World 2!!!”,如头部动作链接部分所示。我们已经知道“Header Only”按钮的工作原理,但另外两个按钮需要更多的编程。主要区别在于,“Header Only”按钮的动作处理和作用域刷新都在一个ScopeHeaderController
中完成,因为我们只需要刷新Header
作用域。对于“Parent Scope”和“First Child Scope”按钮,我们还需要刷新根NonsenseExample2Controller
负责的父ItemRepeater
作用域。因此,我们这里需要做的是刷新Header
作用域,然后告诉父控制器刷新ItemRepeater
。为了在控制器之间进行通信,DaST 提供了控制器动作机制。
从子控制器触发动作
子控制器可以触发动作,该动作可以在父控制器中处理。动作使用RaiseAction()
[*]函数触发,我们传递动作名称和通用参数。该动作可以在父控制器中使用HandleAction()
[*] API 处理,其语法与客户端动作处理一致。唯一的区别是数据项这次不必是 JSON 可序列化的,因为对象是在服务器端传递的。此外,在处理客户端动作时,在调用HandleAction()
[*]之前无需指向作用域,但在控制器动作的情况下,我们必须指向附加有所需子控制器的作用域。回到我们的示例。假设我们输入“Hello World 2!!!”,如头部动作链接部分所示,并单击“Parent Scope”按钮。动作再次由列表 3.2 上的第 33 行触发,但这次目标等于"parent"
。我们列表 3.4 上的第 21 行的动作处理程序再次执行,这次第 27 行的 switch 子句选择"parent"
的情况
31: case "parent": CtrlPath().RaiseAction("RaisedFromChild", "parent"); break;
这是我们触发"RaisedFromChild"
动作的地方,该动作将在父控制器中处理。我将“parent”作为通用数据参数传递,因为我稍后需要使用此值。执行此行后,动作被触发并对父控制器可见,在本例中是列表 3.3 上的根NonsenseExample2Controller
。我们需要处理此动作的第一件事是关联动作处理程序。这在列表 3.3 的第 28-33 行完成,我逐个指向所有Header
作用域(附加有ScopeHeaderController
的作用域),并将Action_RaisedFromChild()
动作处理程序与"RaisedFromChild"
动作关联。因此,列表 3.3 上的第 39 行的Action_RaisedFromChild()
会响应"RaisedFromChild"
动作而被调用,arg
等于"parent"
。第 41 行的if
子句得到满足,第 43 行和第 44 行被执行。CurrPath()
[*]仍然指向Header
作用域,因此,要刷新ItemRepeater
,我们必须首先在作用域树中向后退一步,然后刷新。这在第 43 行完成。在第 44 行,我们向客户端发送一个双工"RefreshedFromServer"
消息,将作用域 ID 作为参数传递,以通知我们的 UI 作用域刷新。在列表 3.1 的根模板中,此消息通过第 52 行的以下代码处理
52: DaST.Scopes.AddMessageHandler("{ScopeID}", "RefreshedFromServer", function (data) 53: { 54: markScopeUpdated(data.ScopeID); 55: hideRepeaterHeaders(); 56: });
这会调用我们从上一节中熟悉的markScopeUpdated()
辅助函数。此函数接受已刷新作用域的 ID 作为参数,我使用我传递给上一个MessageClient()
[*]调用的data.ScopeID
。还能更简单吗?好吧,很酷,但这还没完。在Action_RaisedFromChild()
处理程序完成之后,执行返回到列表 3.4 上的Action_RefreshFromClient()
处理程序,回到第 36 行。第 36、38 和 39 行的语句被执行,我已经在上一节中解释过它们。因此,结果是,Refresh()
[*]在ItemRepeater
和Header
作用域上都被调用。由于这个Header
是ItemRepeater
的子级,所以我们是否在Header
作用域上调用Refresh()
[*]实际上并不重要,因为它作为已刷新作用域的子级无论如何都会更新。接下来需要注意的是,在当前动作处理过程中,我们向客户端发送了两条双工消息:列表 3.3 的第 44 行和列表 3.4 的第 39 行。这反过来意味着客户端将调用两个消息处理程序:一个在列表 3.1 的第 52 行,另一个在列表 3.2 的第 51 行。结果,Header
和ItemRepeater
作用域都获得了粗红色边框。
从父控制器触发动作
现在反过来——父控制器也可以触发动作,该动作在其子控制器中处理。或者更准确地说,是在子控制器上调用动作。动作使用InvokeAction()
[*]函数调用,您可以在其中传递动作名称和通用数据项。在调用此 API 之前,您必须指向附加所需控制器的作用域。其他一切都与上一节中触发的控制器动作非常相似。动作使用统一的语法和HandleAction()
[*] API 进行处理(无需指向作用域,因为动作是按控制器调用的)。现在让我们看看最后一个按钮是如何工作的。假设我们在ItemRepeater
的头部输入了一些“Hello World 3!!!”,并单击“First Child Scope”链接,如头部动作链接部分所示。这是最复杂的情况。在这里,我们不想更新父ItemRepeater
作用域,而是想更新ItemRepeater
的第一个直接子级的Header
作用域。这种操作并没有实际意义——我只是想在这里演示各种更新和控制器通信技术。因此,在这种情况下,子ScopeHeaderController
仍然通知父NonsenseExample2Controller
,然后控制器不是更新ItemRepeater
作用域,而是将通知进一步推送到其第一个子作用域的头部,即推送到另一个子ScopeHeaderController
。让我们看看这在代码中是什么样子。动作以同样的方式通过列表 3.2 的第 33 行触发,但这次目标等于"child"
。我们列表 3.4 的第 21 行的动作处理程序再次执行,这次第 27 行的 switch 子句选择"child"
的情况
32: case "child": CtrlPath().RaiseAction("RaisedFromChild", "child"); break;
因此,我们以类似的方式再次触发 "RaisedFromChild"
控制器操作,就像我们对上一个按钮所做的那样,但这次将 "child"
作为数据传递。第 39 行(在清单 3.3 中)的 Action_RaisedFromChild()
回调被执行,这次流程进入第 46-64 行的第二个 if
分支。现在查看图 3.1 中的作用域树。此处的所有解释都假设我们使用了 ItemRepeater
标题中的按钮。因此,操作是从附加到 Header
作用域的控制器触发的,该作用域的父级是 ItemRepeater
。我们的目的是访问 ItemRepeater
的第一个子 Item 作用域的标题,向 CurrPath()
[*] 传递适当的路径,并通知它该事件。因此,任务就简化为找到正确的路径。知道 CurrPath()
[*] 指向原始 Header
作用域,并查看图 3.1 中的树,不难看出相对路径将是 -1$0-Item$0-Header
,即回到 ItemRepeater
一步,然后到第一个 Item
及其 Header
。问题是,在一般情况下,我们不知道是哪个 Header
触发了操作,我们必须根据当前路径构建所需的路径。这就是我在第 49-58 行所做的。基本上,我获取作用域 ID 上的最后一个段,并根据作用域名称,获取正确的下一个作用域。那个 switch 有点丑陋,但这只是一个快速而粗糙的解决方案。结果是,我们有了路径,并用它在第 62 行的子控制器上调用操作。
62: CurrPath(-1, nextScope, "Header").InvokeAction("InvokedFromParent", null);
我指向 ScopeHeaderController
所附加到的所需 Header
作用域,并调用 InvokeAction()
[*] API,将 "InvokedFromParent"
作为操作名称,将 null
作为通用数据项。在第 15 行的 ScopeHeaderController
中,此操作绑定到第 42 行定义的 Action_InvokedFromParent()
处理程序。此处理程序的代码很简单——首先,我们刷新当前作用域(它是为上一个 InvokeAction()
[*] 调用指向的 Header
作用域),并向客户端发送另一个 "RefreshedFromServer"
消息。因此,这次我们也更新了 2 个作用域并发送了 2 条相应的双工消息:一条用于原始 Header
,另一条用于第一个子作用域的 Header
。此操作的 UI 结果显示在 标题操作链接 部分的最后一张图片中。只有 2 个 Header
作用域更新了,没有其他任何东西。
结论
恭喜!本教程已完成,现在您是 DaST 专家了!我知道这样一套有限的工具可以取代 WebForms 或 MVC 等标准框架是不寻常的,但它确实可以。设计您的网页 UI,将其划分为作用域,并使用 DaST 单独控制您的标记区域——这就是全部。这才应该是 Web 开发的样子——简单直观。无需学习大量的服务器控件、奇怪的页面生命周期事件、网格/中继器/任何绑定表达式等等。DaST 的一个巨大好处是完全分离的表示——HTML 设计团队可以在真实的模板上工作,而开发人员则可以使用骨架模板。此外,每当现有应用程序需要 UI 更改时,HTML 设计师可以在不中断程序员的情况下完成。
当然,ASP.NET DaST 框架仍处于早期阶段。目前,我们有一个定义明确的模式、经过验证的概念和正在工作的渲染核心,这些已经允许我们做很多事情。但未来仍然会添加一些功能。在接下来的几个月里,我想完成以下工作:
- 分离基于 jQuery 的 Ajax 层。摆脱
FORM
标签和页面上所有不必要的 ASP.NET 脚本。 - 使 DaST 完全兼容 MVC 应用程序,这样 MVC 爱好者也能使用它。
如果您想加入该项目(或 PHP DaST)——现在就行动!如果您有任何绝妙的想法/建议,请与我分享。关注更新并关注 Twitter 上的 @rgubarenko。我乐于回答任何类型的问题,并很高兴听到您的反馈。请给我写几行您对此的看法。所有有用的链接如下。
链接
- 项目网站是 www.Makeitsoft.com
- 从 CodePlex 下载所有源代码:aspnetdast.codeplex.com
- 支持、问题、反馈:DaST 支持论坛 或 联系我们
- 关注 @rgubarenko 获取最新新闻和更新