ASP.NET Scopes Framework:Forms和MVC的强大替代方案






4.87/5 (18投票s)
本文介绍了一种全新的Web开发模式,并提供了一个基于该模式的框架。这是一种期待已久的开发方法,它为Forms和MVC的所有基本问题提供了一个独特而优雅的解决方案,并将Web 2.0站点开发任务提升到一个新的灵活性和透明度水平。
摘要
将页面组合成数据绑定的分层范围(scopes)的思想是一种新的Web开发方法,旨在简化富Web 2.0应用程序的构建,最大限度地分离表示层和服务器端逻辑,并最大限度地减少框架架构和基础设施对后端代码设计的影响。通过这种方法生成和呈现数据的方式,带来了新的结构简单性和服务器端代码的透明度,使Web应用程序的后端设计达到完美。
ASP.NET Scopes Framework(以下简称SF)是首次尝试在最流行的Web开发平台上实现该概念,它是标准ASP.NET Forms和MVC开发的一种有趣替代方案。SF中的页面由控制显示的模板和包含所有服务器端逻辑的控制器类组成。模板仅包含符合W3C标准的HTML,没有任何服务器控件。SF将模板视为一组分层结构的HTML片段,称为数据范围(data scopes)。由于其分层结构,我将这组内容称为数据范围树(data scope tree)。树中的每个数据范围都有一组占位符(placeholders),在模板渲染时会被实际数据替换。这些实际数据则由与之关联的控制器生成。控制器为数据范围树中的每个范围单独提供数据,并处理客户端引发的各种事件(或操作 actions)。AJAX和部分更新是通过在由操作发起的异步回发时仅刷新数据范围树中选定的数据范围来实现的。基本上就这些!听起来很简单?实际上比听起来还要简单:)
注意:英语是我的第二语言,因此请容忍并原谅我在整篇文章中出现的语言错误:)
ASP.NET Scopes Framework的当前版本是Alpha 1,这意味着它仍然缺少许多计划中的功能,存在一些错误,并且包含大量粗糙的编码。我还在决定一些框架要求。在这篇文章中,我提供了框架的二进制.dll文件以及学生应用程序演示网站,以便您可以在真实的Web 2.0应用程序中看到新的方法,并感受新的Web开发概念的力量。从框架的第一个稳定版本(我希望在几个月内发布)开始,ASP.NET SF将成为一个开源项目,供所有人使用。在此之前,我不建议在生产应用程序中使用ASP.NET SF的Alpha或Beta版本。
更新(2010年11月23日):我开设了一个公共讨论博客,专门讨论新的Web开发概念和基于它们构建的ASP.NET Scopes Framework。如果您对SF有任何问题或建议,请访问我的博客:www.rgubarenko.net
目录
- 摘要
- 目录
- 1. 学生应用程序演示站点
- 2. 为什么选择Scopes Framework?
- 3. SF中的ASPX页面和顶级请求处理
- 4. SF中的范围树和页面模板
- 5. SF中的渲染过程和控制器类
- 6. SF操作
- 7. 对话框窗口
- 8. 总结
- 结论
1. 学生应用程序演示站点
您首先需要做的是下载本文附带的源代码。代码包含一个基于Scopes Framework(以下简称SF)的演示网站。该网站称为学生应用程序,它是使用VS 2008 ASP.NET 3.5构建的。浏览网站源代码并熟悉其结构——整个网站只有几个文件。在Bin文件夹中,您可能会注意到AspNetScopes.dll二进制文件,其中包含整个框架,必须将其放在希望使用SF的任何应用程序的Bin文件夹中。在本文中,我将逐步研究学生应用程序网站是如何从头开始开发的,并提供所有必要的理论知识和参考。我将本文组织成循序渐进的新内容,即每个SF理论部分之后都有实践部分,我将在其中详细解释理论在学生应用程序演示网站中的应用。
学生应用程序站点由一个默认页面组成,该页面显示了学生记录列表。总共有10名学生,但页面只显示最多3名学生,并在底部有一个分页控件用于分页。每条学生记录包括个人信息和该学生注册的课程列表。学生课程列表也限制为3项,并有自己的分页控件。课程和学生的翻页都是通过AJAX方式完成的,无需更新整个页面。图1显示了学生应用程序默认页面的客户端浏览器外观。
图1:学生应用程序的Default.aspx页面UI
您会注意到学生个人信息下方的“Popup 1”和“Popup 2”按钮。这些按钮用于演示使用SF创建对话框窗口的不同技术。从视觉上看,这两个按钮调用的对话框行为相同(除了第二个对话框的标题栏颜色不同),但底层实现不同:“Popup 2”对话框使用了传统的第三方jQuery插件方法,而“Popup 1”对话框仅使用ASP.NET SF实现,无需任何弹出窗口JavaScript。图2显示了这两个对话框在客户端浏览器中的外观。
![]() |
![]() |
这个应用程序本身功能不多,但它做到了关键的一点——它证明了基于范围的服务器端页面的概念,并展示了围绕该概念构建的SF的强大功能。通过应用我用来构建学生应用程序演示网站的技术,开发人员可以创建自己的基于SF的应用程序,这些应用程序类似于当今WWW上最丰富、最复杂的Web 2.0网站。在本文中,我将尝试结合教程、架构概述和SF Web应用程序的编程参考,以便让您对SF的功能有一个清晰的了解。
注意:学生应用程序不演示客户端输入处理。我将在不久的将来将这部分添加到演示应用程序中。我还打算向客户端SF添加更多功能,以简化用户输入实现。因此,请关注框架更新。
2. 为什么选择Scopes Framework?
在开始讨论SF架构之前,我想稍微谈谈促使我寻找替代Web开发方法的原因,这导致了新概念和围绕它构建的SF的出现。我还想分享我最初的想法,这些想法对框架的整个架构产生了重大影响。
2.1. 表示分离
标准的ASP.NET,除了是最流行的Web开发平台之外,还有一些我们都知道的问题。最大的问题是实际无法进行页面的TDD(测试驱动开发),这是由于服务器端代码与表示层之间缺乏分离所致。MVC Framework是解决可测试性问题的一个优雅方案;然而,表示层分离问题仍然没有解决。我将详细解释这一点。
我认为你们都同意,编程过程中最烦人的事情之一是实际的开发工作必须多次在图形设计师和程序员之间来回进行。他们会来问你问题,比如“哦,我们需要一个小代码更改来交换这张表中的两列”或“哦,我无法通过CSS调整这个布局,似乎需要你做一些代码更改”。这些小事情实际上浪费了大量时间和金钱,一次又一次地打断程序员正在进行的工作,直到达到所需的GUI并让客户满意。无论是标准的ASP.NET Forms还是MVC,都无法让程序员和图形设计师分开工作。在Forms中,.aspx/.ascx标记包含大量的服务器控件,开发人员必须从一开始就插入这些控件来测试页面的整体功能。只要表示层的功能控件与后端绑定,从定义上来说就是不可能分离的。此外,图形团队通常难以理解如何调整页面上服务器控件的外观。MVC使情况变得更糟,在视图(.aspx)和部分视图(.ascx)中引入了控制流语句,粉碎了Web开发人员分离图形设计师工作与应用程序编程的最后希望。
因此,在设计SF时,我提出的第一个简单想法是,服务器端编程应与表示层完全独立。反之,表示层应由图形专家完成,而无需应用程序开发人员进行代码更改。更重要的是,设计师应该有100%的自由来处理表示层标记的任何内容,而不会对整个应用程序功能产生任何影响,就像.aspx/.ascx一样,不小心更改标记可能会导致整个应用程序崩溃。您可能现在认为完全的表示层分离太好了,不可能是真的?实际上并非如此,这个概念是完全可行的,甚至实现起来也很简单。
2.2. Web应用程序重访
进一步发展分离的理念,我得到了一个百万美元的问题……什么是Web应用程序?我的答案是,从技术上讲,Web应用程序是一系列由服务器端逻辑生成并呈现给客户端浏览器用户的*数据*。仔细想想。您的服务器端代码执行直到数据生成,然后,表示引擎获取数据并将其插入页面,为最终用户生成最终输出。就是这样!服务器端只负责生成数据,而不了解表示层的任何信息,而表示层只期望数据,完全忽略数据是如何生成的。
但是,现有的ASP.NET应用程序中现在发生了什么?后端代码总是与表示层混合在一起,因为标记中有服务器控件。开发人员总是需要担心页面生命周期、事件顺序、控件在哪里以及何时进行数据绑定、如何确保某些代码部分不在异步回发时执行等等。结果是,您的后端设计严重受到框架特性的影响。
因此,除了完全的表示层分离之外,寻找更好的开发方法的另一个原因是我想让开发人员能够完全专注于应用程序设计,而不是思考如何将他们的设计理念更好地融入框架的现有架构中。
2.3. 开发速度 vs. 灵活性 vs. 其他属性
总的来说,为了完成开发工作,我们软件开发人员使用不同的语言、平台、框架、API、工具等。在这些之间进行选择时,我们必须比较特定的开发过程属性,如开发速度、功能灵活性、功能可用性、最终性能等。通常这些属性是相互排斥的,所以选择特定的开发工具时,我们会偏重某些属性而忽略其他属性。例如,我们可以使用CMS(如DotNetNuke)来加速网站构建,这效果很好,只要我们不需要对特定模块进行深度定制。因此,基于CMS的解决方案只在一定程度上灵活。或者,我们可以使用低级编程语言来更好地控制系统,但即使是简单的软件产品也需要大量时间才能投入生产。因此,我们为了另一方的软件属性而牺牲一个软件属性的情况在编程世界中是很正常的。
通过开发模板化数据范围的概念和基于它们构建的框架,我试图打破这种依赖。您将看到SF如何极大地加速Web 2.0网站的开发,而不会损失任何灵活性。同时,与标准Forms渲染或MVC相比,SF在性能方面非常轻量级。SF我最喜欢的功能之一是它的设计使您的后端代码绝对透明且美观。
2.4. SF与ASP.NET Forms
我在实现之初设定的一个条件是,SF必须与标准的ASP.NET共存,而不否定其任何功能。相反,SF应该补充Forms,允许程序员使用所有有价值的标准ASP.NET功能,如会话跟踪、增强的安全性、基于角色的成员资格、注册客户端脚本、数据缓存等。
此外,我希望SF页面在同一个应用程序中与Forms页面共存。这种设计允许轻松地将基于Forms的应用程序或其特定部分迁移到更灵活的基于SF的应用程序。与Forms一样,SF中的请求处理也应该基于请求URL中指定的物理页面位置。这与MVC不同,MVC中的请求URL不包含应用程序资源的物理路径,而是根据请求URL中的定义模式选择处理控制器。在这种情况下,我偏爱Forms而不是MVC方法,因为在我看来,在请求URL中反映物理页面位置从开发角度来看更透明和一致。而当我们还需要搜索引擎友好的URL时,我们可以随时插入并使用现有的URL重写模块之一。
总而言之,大约6个月前,我提出了完全模板化的服务器端页面的想法,这些页面使用树状结构的数据范围来表示数据,其中每个数据范围都可以单独刷新,以实现无限的AJAX类功能。因此,我启动了导致ASP.NET Scopes Framework的项目,本文正是关于这个项目的。
3. SF中的ASPX页面和顶级请求处理
3.1. 模板和控制器
为了处理传入的请求,执行业务逻辑并渲染输出,SF使用控制器/模板对,这是SF对标准ASP.NET Forms应用程序中使用的.aspx(.ascx)/codebehind对的替代。因此,为了在SF中创建一个页面,开发人员必须完成2个步骤:
- 创建一个包含所有服务器端逻辑的控制器类。
- 创建一个驱动整个页面显示的模板文件。
为了让系统知道当前请求应该由特定的控制器/模板对服务,开发人员会创建一个.aspx页面,该页面将传入请求与特定的控制器关联起来进行处理。这个.aspx页面不包含任何逻辑——SF仅将其用作请求入口点,将所有后续工作转移到所需的控制器。
3.2. 顶级执行流程
图3显示了SF页面如何处理传入请求的高级步骤(蓝色圆圈编号)。最初,请求以常规方式到达.aspx页面(步骤1)。该页面包含特殊的标记,告诉系统这实际上是一个SF页面。页面还指定了将用于处理的控制器(步骤2)。然后,控制权转移到SF引擎,该引擎执行控制器中的逻辑并生成输出数据(步骤3)。然后使用与控制器关联的模板来表示数据并生成最终输出(步骤4)。最后,控制权返回到.aspx页面,该页面获取输出并将其添加到页面响应中(步骤5)。
图3:SF中的顶级请求处理步骤
3.3. 使.aspx能作为SF页面工作
为了使.aspx页面能够作为SF页面工作,开发人员必须使用SF提供的特殊ScopesManagerControl
。图4中的图表显示了该控件类的成员。它还显示了一个额外的ProvideRootControlEventArgs
类,该类被控件用作其ProvideRootControl
事件处理程序的参数类型。
图4:ScopesManagerControl和ProvideRootControlEventArgs类图
要将常规的.aspx页面转换为SF页面,开发人员需要进行以下更改:
- 在.aspx页面上,在
ScriptManager
之后添加ScopesManagerControl
。 - 处理
ScopesManagerControl.ProvideRootControl
事件。在事件处理程序实现的体内,将所需控制器的实例分配给作为处理程序参数传递的ProvideRootControlEventArgs
对象的RootScopeControl
属性。 - 在.aspx标记中添加head和body内容字面量,并通过其
BodyLiteralID
和HeadLiteralID
属性将它们的ID传递给ScopesManagerControl
。
现在,图3中的步骤2和5是如何实际工作的就更加清楚了。在步骤2中,SF需要获取用于进一步处理的实际控制器。我们使用ProvideRootControl
事件处理程序显式地将控制器的实例传递给SF。在步骤5中,所有处理都已完成,输出已准备就绪。.aspx页面上的ScopesManagerControl
使用字面量将控制器/模板对的head和body输出插入到页面中,然后再将响应发送给最终用户。
注意:手动修改.aspx以将请求入口点绑定到控制器是Alpha版本中使用的快速粗糙解决方案。它有效,但在未来的版本中需要更改为某种更好的选项,该选项不需要手动标记以及用于插入输出的head和body字面量。我仍在权衡各种方法的优缺点,并将在SF的下一个Beta版本中尽量确定最终设计。
3.4. 实践:学生应用程序中的Default.aspx
正如我已经提到的,我们简单的学生应用程序由一个默认的Default.aspx页面组成,该页面显示了图1和图2中描绘的UI。该页面是SF的请求入口点,提供控制器/模板对来处理请求并进行输出。以下是Default.aspx标记的列表。
列表1:~/Default.aspx 1 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
2 <%@ Register assembly="AspNetScopes" namespace="AspNetScopes.Framework.Controls" tagprefix="AspNetScopes" %>
3
4 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
5
6 <html xmlns="http://www.w3.org/1999/xhtml">
7 <head id="Head1" runat="server">
8 <asp:Literal ID="LiteralHeadContent" runat="server"></asp:Literal>
9 </head>
10 <body>
11 <form id="form1" runat="server">
12 <asp:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="true" ScriptMode="Debug">
13 </asp:ScriptManager>
14 <AspNetScopes:ScopesManagerControl ID="ScopesManagerControl1" runat="server"
15 HeadLiteralID="LiteralHeadContent"
16 BodyLiteralID="LiteralBodyContent"
17 onproviderootcontrol="ScopesManagerControl1_ProvideRootControl" />
18
19 <div>
20 <asp:Literal ID="LiteralBodyContent" runat="server"></asp:Literal>
21 </div>
22
23 </form>
24 </body>
25 </html>
下一个列表是Default.aspx.cs代码隐藏文件。
列表2:~/Default.aspx.cs 1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Web;
5 using System.Web.UI;
6 using System.Web.UI.WebControls;
7
8 using AspNetScopes.Framework.Controls;
9
10 public partial class _Default : System.Web.UI.Page
11 {
12 protected void ScopesManagerControl1_ProvideRootControl(object sender, ProvideRootControlEventArgs e)
13 {
14 e.RootScopeControl = new PageStudents();
15 }
16 }
正如您所见,该页面是根据我们在上一节中讨论的规则构建的。在列表1的第14行,ScopesManagerControl
在ScriptManager
之后添加到页面。用于将渲染HTML的head和body部分插入到最终输出中的字面量分别在第8行和第20行。在第15行和第16行,这些字面量的ID被传递给ScopesManagerControl
。在第17行处理了ProvideRootControl
事件。在列表2的第12-15行实现了事件处理程序,并通过参数对象的RootScopeControl
属性将PageStudents
控制器的实例显式传递给SF。
所以,这里的一切都非常简单。当请求到达Default.aspx页面时,ScopeManagerControl
会触发ProvideRootControl
事件,获取PageStudents
控制器的实例,并使用该控制器进行所有后续处理,直到输出准备就绪。
4. SF中的范围树和页面模板
4.1. 模板概念
回想一下我们在第2.2节中对Web应用程序的讨论。SF旨在最大限度地实现表示层分离,因此位于控制器类中的后端代码仅负责生成数据,这些数据通常是一系列字符串值。然后,表示引擎获取所有这些数据值,并使用与控制器关联的模板来渲染最终输出。
那么,SF中的数据是如何渲染的呢?渲染意味着构建呈现给最终用户的最终HTML输出。想象一下,我们要为图1中一个学生记录的学生个人信息区域构建HTML输出。考虑到我们已经有学生姓名、SSN、专业等值的列表,以“表示分离”最佳的方式该如何做到?我的答案是,渲染这些值的最简单、最灵活的方法是使用纯HTML模板,并在渲染阶段用相应的替换值替换占位符!是的,只有符合W3C标准的、漂亮的、带占位符的HTML,除此之外别无其他!没有服务器控件,没有控制流语句,没有内联数据绑定,没有任何东西能告诉我们服务器端处理的存在。接下来,如果页面的某个区域可以用带占位符的HTML片段表示,那么整个页面就可以用一组不同的带占位符的HTML片段来表示。我们所需要做的就是拥有一系列字符串值来替换占位符,以获得最终的HTML输出!关于纯HTML模板的这个简单想法,实际上就是本文所讨论的基于范围的服务器端页面的整个概念的根本思想。
现在我们将形式化这些内容,并推导出数据范围(data scopes)的概念,它是HTML模板中HTML片段的逻辑表示。
4.2. 数据范围
数据范围是SF的核心概念,也是贯穿始终的主要逻辑单元。抽象地说,数据范围是一组强内聚的数据值。在数据范围内,数据值可以根据更窄的内聚标准进一步分组到较小的数据范围中。这意味着数据范围可以包含任意数量的子数据范围。物理上,数据范围是包装在DIV
标签中的HTML片段,该标签具有属性scope
,其值是开发人员选择的某个范围名称。范围的名称必须与其直接父级的范围唯一。每个数据范围在其包装DIV
标签内可以有多个占位符。占位符是字符串标记,在数据范围渲染时会被相应的数据值替换。在渲染过程中,每个数据范围可以多次重复其包装DIV
标签的内容,这就是SF中实现重复器(repeater)功能的方式。与其说范围DIV
重复其内容,不如说范围被重复。通常我们不想重复范围的全部内容,而只想重复其中一部分。例如,如果一个网格由一个范围DIV
内的表表示,我们只想重复表行,而不是表本身。为此,您可以使用“<!--scope-from-->”和“<!--scope-stop-->”注释来标记数据范围中要重复内容的开始和结束。数据范围方法允许SF优雅地实现AJAX。SF中的每个数据范围都可以单独刷新,提供最强大、最透明的部分更新功能,无需额外编码。
现在我们可以用数据范围来重新定义我们的HTML模板。HTML模板只是一组数据范围,每个Web页面都可以使用一组数据范围以特定的嵌套配置进行渲染。如何将页面划分为数据范围以及使用什么内聚标准完全取决于开发人员,但这个过程相当简单。例如,在图1中,单个学生记录可以由Student范围表示,该范围又可以进一步划分为由Profile范围表示的个人信息区域和由Schedule范围表示的课程安排区域。表示学生记录的实际HTML模板的一部分可能看起来像这样:
图5:HTML模板的一部分示例
请注意,在图5中,我将范围称为StudentRepeater而不是简单的Student,因为我想强调此范围的内容被重复以显示多条记录。我使用了“<!--scope-from-->”和“<!--scope-stop-->”注释来缩小重复内容的范围,因为我只需要重复包含Profile和Schedule范围的表行。在Profile范围中,我指定了两个示例占位符——“{StudentName}”和“{StudentSSN}”——它们在页面渲染时会被相应的数据值替换。占位符不一定必须是花括号括起来的标记格式——它只是一个字符串,通过简单的查找和替换操作在数据范围内被另一个字符串替换,所以格式确实可以是任何东西。
4.3. 范围树
我已经提到,HTML模板中的数据范围具有分层结构,所以我称这种结构为范围树(scope tree)。在规划HTML模板结构时,将HTML模板视为范围树非常方便且直观。在SF内部,HTML模板和渲染结果也由范围树数据结构表示。开发人员可以在控制器类中访问这些数据结构来操作树中的特定数据范围。
代表HTML模板中DIV
标签结构的范围树称为模型范围树(model scope tree)。让我们为图5中的HTML模板的一部分描绘模型范围树。我们只需将HTML模板的分层DIV
结构转换为节点名称为相应范围的树。结果如图6所示。
图6:图5中HTML模板的模型范围树
在模板渲染完成后,结果由渲染范围树(rendered scope tree)表示。它可能与模型范围树不同,因为在渲染过程中,某些范围可以与其所有子数据范围一起重复其内容。因此,渲染范围树会获得来自重复其内容的范围对应节点的新的范围分支。如果没有范围被重复,那么渲染范围树与模型范围树相同。否则,模型范围树是渲染范围树的子树。假设我们的学生重复了3次,现在让我们描绘代表图5中部分HTML模板渲染后最终HTML输出的DIV
标签结构的渲染范围树。如果有3个学生,那么Profile和Schedule范围就重复了3次。我在较暗的背景上放置了树的模型部分,以强调渲染范围树只是模型树的某些分支被乘以了。结果如图7所示。
图7:图5中HTML模板的渲染范围树(学生记录重复3次)
范围树非常适合表示分离的概念。开发人员需要做的就是创建一个模型范围树,它在物理上只是一组嵌套的DIV。模型范围树明确定义了HTML模板的结构,而系统仅凭此就足够使用此模板来显示控制器中的数据。一旦模型树准备就绪,包含只有占位符的DIV
标签的骨架模板就可以交给HTML专家。然后,该专家可以对模板进行任何操作,只要保持范围树结构不变。因此,开发人员不必关心任何一行标记,而设计师也无需了解服务器端的任何知识,只需保持范围DIV
标签的嵌套结构不变。
4.4. 实践:学生应用程序中的范围树
创建定义HTML模板的模型范围树的过程实际上需要坐下来用笔和纸。在上一节中,我们已经为图5中的一小部分HTML模板创建了模型和渲染范围树。现在,同样地,我们为图1的整个页面输出构建一个完整的HTML模板的模型范围树。显然,HTML模板中的范围DIV
标签可以以多种不同的方式组合,并且没有严格的规则来构建模型树,但在实践几次之后,这个任务对任何技能水平的开发人员来说都变得相当简单。
所以,现在看图1。我们需要规划数据范围的结构并决定需要哪些占位符。这里的推理非常简单。让我们从我们在第4.2节中已经熟悉的那个学生记录开始。所以,我们需要一个包含Profile范围和Schedule范围的StudentRepeater范围。在Profile范围内部,我们需要占位符来显示学生姓名、SSN等。让我们坚持使用我们在第4.2节中已经使用的格式,所以占位符标记是“{StudentName}”、“{StudentSSN}”等。接下来,Schedule范围包含一个重复学生课程的区域、一个用于课程分页的分页控件,以及一个显示当前显示课程范围的简短摘要。我们将这三个区域表示为三个不同的范围:CourseRepeater、Pager和Summary。CourseRepeater没有子范围,只有用于显示课程ID、姓名和时间的占位符。此范围被重复以显示多条课程记录。Summary范围也只有用于显示页面范围内开始和结束页码的占位符。Pager范围更有趣。它必须显示上一页和下一页按钮,并在当前页面为0时禁用上一页按钮,在当前页面为最后一页时禁用下一页按钮。这通过将4个新的子范围嵌套到父Pager范围中来实现:PrevDisabled、PrevEnabled、NextEnabled和NextDisabled。其思想是,当相应的按钮启用时显示启用的范围,否则显示禁用的范围。现在我们完成了学生记录部分。我们需要在StudentRepeater下方再有一个分页控件来为学生记录分页。我们也将它称为Pager范围,这个范围的结构与用于分页学生课程的第二个Pager范围完全相同。最后,为了有一个更整洁的结构,让我们将StudentRepeater和Pager范围包装在GridArea数据范围中。这样就完成了!模型范围树已经准备好了,这自动意味着骨架HTML模板也准备好了,因为它只是模型范围树的物理反映。
图8显示了学生应用程序的Default.aspx页面的模型范围树和完整的渲染范围树。图8包含许多我们尚未讨论过的细节。这是因为我决定使用这个单一的图来可视化所有后续的SF架构解释和讨论,而不是使用多个小的图。所以,目前我们只对黄色背景上、标题为“模型范围树”的较暗灰色背景中的模型树感兴趣。相应范围的占位符显示为浅灰色字体的树节点注释。
图8:学生应用程序的模型和渲染范围树
首先,您应该注意到范围树的根部有一个NULL范围。NULL范围用作页面上所有其他数据范围的容器。如果模板中没有范围,那么范围树将只包含一个根节点,即NULL范围。根范围被系统视为与其他任何数据范围一样,不同之处在于,所有其他数据范围在物理上都由HTML模板内的DIV
标签包装的HTML片段表示,而NULL范围的HTML片段是整个HTML模板,因此在其内容周围没有DIV
标签。放置在所有其他范围之外的占位符属于根范围。
其次,我们还有PopupPlaceholder和Popup2Placeholder数据范围,我们尚未讨论过。这两个是“Popup 1”和“Popup 2”功能所必需的,如第1节所述。在图8中,我懒得绘制从这些范围出来的完整的模型树分支,只是用省略号表示它们,但实际上这两个数据范围有类似从StudentRepeater范围出来的分支。我将在文章结尾简要解释弹出窗口,但现在请忽略这两个范围。
第三,CourseRepeater范围只有标记和里面的占位符,不包含任何子范围,但由于其内容被重复,我需要在图8中以某种方式强调这一点,所以我只用省略号表示内容。
我们完成了模型树!实际上,对于任何复杂程度的网页,想出一个最优的范围树结构非常简单。再说一次,我们不需要为页面做任何标记——我们所需要的只是一个10分钟内完成的结构,其余的一切完全取决于图形设计师。既然我们有了模型树,就来看看渲染后会是什么样的渲染范围树,这很有意思。
假设学生记录重复了3次,每个学生记录的课程也重复了3次,如图1的截图所示。生成的渲染范围树在图8中显示为树的组合,背景色为较暗的灰色和浅灰色。我已经提到,模型范围树总是渲染范围树的子树,所以图8很好地说明了这一点。模型范围树中StudentRepeater和CourseRepeater旁边的粗体白色箭头表示这些范围的内容被重复了。根据假设,CourseRepeater重复其内容3次。在图8的模型树节点CourseRepeater范围中,您可以看到其内容用省略号表示,重复了3次。我特意将其中两个省略号放在浅灰色背景上,以强调重复的内容已经属于渲染范围树。接下来,看StudentRepeater。它有两个分支,分别来自Profile和Schedule范围。在渲染范围树中,这些分支被重复了3次,所以我放了两个额外的分支在浅灰色背景上,表示这些分支只在模型树渲染时出现。
好的,我希望您已经明白了模型范围树如何变成渲染树。虽然这里的一切都很简单,但在继续深入之前,您应该确保完全理解了范围和树,因为我们将在SF中看到的其他所有内容都是围绕数据范围和范围树构建的。
4.5. 实践:StudentsPage.htm模板
现在是时候构建实际的模板了。首先,我将构建一个仅包含范围和占位符的骨架模板,没有任何标记。然后,我将提供我们演示应用程序中使用的模板,以便您可以比较差异。要构建由模型范围树表示的骨架模板,我们实际上不需要做任何事情。这只是一个简单的逻辑范围结构到物理DIV
结构的转换。为了使骨架模板更小,我忽略了两个弹出窗口的数据范围。以下是与图8中的模型范围树对应的骨架模板的列表。
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml">
3 <head>
4 <title></title>
5 <style>div {border:solid 1px black;margin:5px;padding:5px;}</style>
6 </head>
7 <body>
8 <div scope="GridArea">
9 <div scope="StudentRepeater">
10 <div scope="Profile">
11 {StudentName} {StudentSSN} {Major} {PhoneNumber}
12 </div>
13 <div scope="Schedule">
14 <div scope="CourseRepeater">
15 {CourseID} {CourseFullName} {StartTime} {EndTime}
16 </div>
17 <div scope="Summary">
18 {FromCourse} {ToCourse}
19 </div>
20 <div scope="Pager">
21 <div scope="PrevDisabled"></div>
22 <div scope="PrevEnabled">{PrevPageIdx}</div>
23 {CurrPageNum} {TotalPageCount}
24 <div scope="NextEnabled">{NextPageIdx}</div>
25 <div scope="NextDisabled"></div>
26 </div>
27 </div>
28 </div>
29 <div scope="Pager">
30 <div scope="PrevDisabled"></div>
31 <div scope="PrevEnabled">{PrevPageIdx}</div>
32 {CurrPageNum} {TotalPageCount}
33 <div scope="NextEnabled">{NextPageIdx}</div>
34 <div scope="NextDisabled"></div>
35 </div>
36 </div>
37 </body>
38 </html>
我只添加了一些CSS来使该结构在浏览器中看起来更整洁。图9显示了骨架HTML模板在IE窗口中的外观。
图9:IE中的骨架HTML模板
虽然此页面与图1的截图差别很大,但从范围树的角度来看,这两个页面是相同的,我们的骨架模板可以通过添加更多HTML标记和CSS轻松转换为所需的模板,同时保持范围树的结构相同。因此,最后,列表4显示了学生应用程序中使用的完整StudentsPage.htm模板。这就是图形设计师完成工作后的样子。请注意范围DIV
标签的结构——它与骨架模板中的相同。
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2
3 <html xmlns="http://www.w3.org/1999/xhtml">
4 <head>
5 <title></title>
6 <style type="text/css">
7 body, html {font-family:Verdana, Arial; font-size:14px;}
8 .jqmOverlay {background-color:Black;}
9 </style>
10 <script type="text/javascript" src="res/Scripts/jquery.min.js"></script>
11 <script type="text/javascript" src="res/Scripts/jqModal.js"></script>
12 </head>
13 <body>
14 <div scope="GridArea" style="margin:20px;">
15 <div style="padding: 10px; display:block; font-weight:bold; background-color: #800000; height: 20px; color: #FFFFFF;" >
16 GRID DISPLAYING STUDENTS
17 </div>
18 <div scope="StudentRepeater">
19 <table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin- top:10px;">
20 <!--scope-from-->
21 <tr>
22 <td colspan="3" style="height:2px; background-color: #808080;"></td>
23 </tr>
24 <tr>
25 <td valign="top" align="left" style="width:300px;">
26 <div scope="Profile">
27 <div style="display:block;height:120px;">
28 <table cellpadding="2px" border="0px" cellspacing="2px" width="100%" bgcolor="White">
29 <tr><td style="font-weight: bold; background-color: #808080" colspan="2">Student Profile</td></tr>
30 <tr>
31 <td align="right" style="background-color: #808080; font-weight: bold;">Name:</td>
32 <td style="background-color: #CCCCCC"> {StudentName}</td>
33 </tr>
34 <tr>
35 <td align="right" style="background-color: #808080; font-weight: bold;">SSN:</td>
36 <td style="background-color: #CCCCCC"> {StudentSSN}</td>
37 </tr>
38 <tr>
39 <td align="right" style="background-color: #808080; font-weight: bold;">Major:</td>
40 <td style="background-color: #CCCCCC"> {Major}</td>
41 </tr>
42 <tr>
43 <td align="right" style="background-color: #808080; font-weight: bold;">Phone:</td>
44 <td style="background-color: #CCCCCC"> {PhoneNumber}</td>
45 </tr>
46 </table>
47 </div>
48 <div style="display:block; margin-top: 2px;">Student enrolled in {CourseCount} course(s)</div>
49 <div style="display:block; margin-top: 2px;">
50 <input type="button" value="Popup 1" onclick="AspNetScopes.Action('OpenPopup1', '{StudentSSN}')" />
51 <input type="button" value="Popup 2" class="show-modal-popup2" studentSSN="{StudentSSN}" />
52 </div>
53 </div>
54 </td>
55 <td style="width:2px;" valign="top">
56 </td>
57 <td valign="top" align="left" style="background-color: #CCCCCC">
58 <div scope="Schedule" style="height:100%;">
59 <div scope="CourseRepeater" style="height: 120px; ">
60 <table cellpadding="2px" border="0px" cellspacing="2px" width="100%" bgcolor="White">
61 <tr>
62 <td style="font-weight: bold; background-color: #808080">Course ID</td>
63 <td style="font-weight: bold; background-color: #808080">Full Name</td>
64 <td style="font-weight: bold; background-color: #808080">Time</td>
65 </tr>
66 <!--scope-from-->
67 <tr>
68 <td style="background-color: #CCCCCC">{CourseID}</td>
69 <td style="background-color: #CCCCCC">{CourseFullName}</td>
70 <td style="background-color: #CCCCCC">{StartTime} - {EndTime}</td>
71 </tr>
72 <!--scope-stop-->
73 </table>
74 </div>
75 <div style="margin-top: 2px;display:block;">
76
77 </div>
78 <div style="margin-top: 2px;display:block;">
79 <div scope="Pager" style="display:inline;margin-right:20px;margin-left:5px;background-color:#CCCCCC;" />
80 <div scope="Summary" style="font-style: italic;display:inline;">
81 Displayed courses from {FromCourse} to {ToCourse}
82 </div>
83 </div>
84 </div>
85 </td>
86 </tr>
87 <tr>
88 <td colspan="3" style="height:2px; background-color: #808080;"></td>
89 </tr>
90 <tr>
91 <td colspan="3"> </td>
92 </tr>
93 <!--scope-stop-->
94 </table>
95 </div>
96 <div scope="Pager" style="display:block;background-color:#CCCCCC;padding:5px;" />
97 <div style="display:block; background-color: #800000; height:10px;">
98 </div>
99 </div>
100 Popup window invoked by "Popup 1" button is implemented <br />
101 using pure ASP.NET Scopes approach. Dialog invoked by <br />
102 "Popup 2" button demonstrates well known common approach <br />
103 to modal windows using 3rd party jQuery plugin. <br />
104 <div scope="PopupPlaceholder" />
105 <div scope="Popup2Placeholder" />
106 </body>
107 </html>
如果您仔细看过此列表,您会发现两个Pager范围的内容都消失了。这是因为我没有为两个外观相似的分页控件重复内容,而是将标记提取到了与子控制器关联的部分HTML模板中,这相当于ASP.NET Forms中的用户控件和MVC中的部分视图。稍后我将详细讨论子控制器和分页控件的实现。
在浏览器窗口中,我们的HTML模板看起来如下。
图10:IE中的HTML模板StudentsPage.htm
渲染后,此模板将产生我们想要的输出,如图1所示。我们还看到,在处理HTML模板时,图形设计师在浏览器窗口中获得了100%的所见即所得!这是SF的另一个巨大优势,因为尽管集成的Visual Studio设计器是一个很好的工具,但在使用Forms或MVC进行开发时,我们通常会在实际浏览器中获得完全不同的输出。此外,集成的VS设计器在正确渲染嵌套控件时经常失败。使用目标浏览器来处理模板可以完全解决这个问题。
最后,要渲染这个漂亮的HTML模板,我们只需要数据。现在我们来到SF编程的主要部分,即控制器类,其职责是生成要替换占位符的值列表。但在我们开始这个长篇讨论之前,我们需要了解用于导航模型和渲染范围树的范围路径。
4.6. 范围路径
控制器类用于生成数据,这些数据由开发人员单独绑定到每个范围。为了在控制器中操作某个特定的范围,开发人员必须首先选择它。回想一下,在渲染范围树中,范围是可以重复的;因此,我们需要一种方法来区分,例如,第一个、第二个和第三个学生的CourseRepeater(参见图8)。这正是我们需要范围路径的原因。
范围路径只是在范围树中到某个范围的唯一路径。它由从根到所需范围在范围树中访问的节点集表示。每个访问的节点由一个段(segment)组成,该段包含一个重复轴(repeating axis)(重复范围的索引)和一个范围名称。在渲染范围树中,重复轴是0或任何正整数。在模型范围树中,不使用重复轴,因为范围不会被重复。请注意,我们不必在任何地方指定NULL范围,因为每个路径都从它开始。例如,要选择图8中的模型范围树的CourseRepeater,表达式将是:
Select("GridArea", "StudentRepeater", "Schedule", "CourseRepeater")
因此,我们只需按名称列出路径中的节点。在模型范围树中,这可以正常工作,因为范围不会被重复,并且仅使用名称链,我们就唯一地确定了数据范围在树中的位置。在渲染范围树中,此表达式将始终选择第一个学生的CourseRepeater。如果我们想选择第二个或第三个学生的呢?选择所有3个学生渲染范围树中CourseRepeater范围的表达式如下:
Select("GridArea", "StudentRepeater", 0, "Schedule", "CourseRepeater")
Select("GridArea", "StudentRepeater", 1, "Schedule", "CourseRepeater")
Select("GridArea", "StudentRepeater", 2, "Schedule", "CourseRepeater")
这里发生的是,我们必须为Schedule范围指定重复轴才能遵循正确的路径。在渲染范围树路径中指定0
重复轴不是必需的,因为轴默认就是0。这使得我们可以拥有更短、更易读的范围路径选择表达式,以便在控制器中使用。
由于范围路径在渲染的范围树中是唯一的,使用范围路径作为相应范围DIV
标签的客户端标识符是一个简单且可预期的解决方案。也就是说,除了HTML模板中范围DIV
已有的属性之外,当范围被渲染时,还会插入一个id
属性,其值等于范围路径的客户端表示。范围路径的客户端表示由以“$”分隔的段组成。每个段的格式为“<axis>-<name>”。在客户端范围路径中,始终指定轴,即使它是0。这是为了提供一种一致的方式来使用document.getElementById()
或jQuery
选择器访问范围DIV
标签。
4.7. 实践:学生应用程序中的范围路径
系统向范围DIV
标签添加唯一的ID
属性,因为SF的客户端需要知道哪些范围DIV
标签需要在异步回发时更新。范围DIV
的ID
属性是将客户端呈现页面与服务器端控制器类连接的唯一链接。
让我们检查一下学生应用程序中一些渲染范围DIV
标签的ID
属性。看看浏览器中Default.aspx页面的结果输出,并与图8中的渲染范围树进行比较。您可以确保渲染范围DIV
标签的嵌套结构与渲染范围树中所述的完全一致。不要忘记我们只讨论范围DIV
标签,而不讨论页面上任何其他仅用于标记的DIV
标签。所有渲染范围DIV
标签中的scope
属性都已替换为ID
属性,其值等于它们在渲染范围树中相应的路径。
对应于GridArea范围的DIV
渲染为
<div id="0-GridArea" style="margin:20px;">
这个DIV
有两个子DIV
标签,用于渲染StudentRepeater和Pager范围,如下所示:
<div id="0-GridArea$0-StudentRepeater">
<div id="0-GridArea$0-Pager" style="display:block;background-color:#CCCCCC;padding:5px; ">
现在,回想一下我们在第4.6节中使用的示例,我们在控制器类中使用了3个不同的Select()
表达式,传递了范围路径来访问第1、第2和第3个学生的CourseRepeater范围。我们可以看到,这些重复的CourseRepeater范围被渲染为3个DIV
标签,每个学生一个,如下所示:
<div id="0-GridArea$0-StudentRepeater$0-Schedule$0-CourseRepeater" style="height: 120px; ">
<div id="0-GridArea$0-StudentRepeater$1-Schedule$0-CourseRepeater" style="height: 120px; ">
<div id="0-GridArea$0-StudentRepeater$2-Schedule$0-CourseRepeater" style="height: 120px; ">
因此,范围路径的机制相当简单。范围DIV
标签的ID
属性反映了在控制器类中用于选择它们的路径。我建议您花一些时间用笔和纸仔细规划您的模型范围树,因为这有助于更好地看清整体图景,并在控制器类中毫不费力地编写正确的Select()
表达式。
5. SF中的渲染过程和控制器类
5.1. 控制器类概述
控制器类位于一个名为*.cs*的扩展名的文件中。SF中的每个控制器都必须继承自ScopeControl
抽象类。图11中描绘了ScopeControl
抽象类的图。
图11:ScopeControl类图
典型的控制器类继承自ScopeControl
,包含多个方法。在顶级,这些方法可以根据其职责分为4个主要组:
SetTemplate()
方法——用于将特定的HTML模板与控制器关联起来。SetupModel()
方法——用于添加子控制器,附加操作处理程序,以及附加数据绑定处理程序。- 操作处理程序(Action handlers)——用于在异步回发时处理操作。
- 数据绑定处理程序(Data binding handlers)——用于为范围树中的范围提供数据。
5.2. 实践:PageStudents.cs控制器列表
在第3.4节的列表2中,我们将PageStudents
控制类的一个实例传递给了SF核心,告知系统所有后续处理和渲染都必须由PageStudents
控制器完成。除了模拟数据层的DataFacade.cs类(本文未讨论)之外,学生应用程序的所有其他后端代码都位于控制器类中。让我们开始讨论PageStudents
控制器,先给出该类的完整代码列表。请不要试图理解控制器实现的细节——在接下来的讨论中,我将解释该类代码的每一行。以下是PageStudents.cs文件的完整列表。
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Web;
5 using System.IO;
6 using System.Web.Hosting;
7
8 using AspNetScopes.Framework;
9
10 /// <summary>
11 /// Summary description for PageStudents
12 /// </summary>
13 public class PageStudents : ScopeControl
14 {
15 public override void SetTemplate(ControlTemplate template)
16 {
17 template.Markup = File.ReadAllText(HostingEnvironment.MapPath("~/App_Data/Templates/StudentsPage.htm"));
18 }
19
20 public override void SetupModel(ControlModel model)
21 {
22 model.Select("GridArea", "Pager").SetControl(new PagerControl());
23 model.Select("GridArea", "StudentRepeater", "Schedule", "Pager").SetControl(new PagerControl());
24 model.Select("PopupPlaceholder").SetControl(new PopupControl());
25 model.Select("Popup2Placeholder").SetControl(new Popup2Control());
26
27 model.Select("GridArea").SetDataBind(new DataBindHandler(GridArea_DataBind));
28 model.Select("GridArea", "StudentRepeater").SetDataBind(new DataBindHandler(StudentRepeater_DataBind));
29 model.Select("GridArea", "StudentRepeater", "Profile").SetDataBind(new DataBindHandler(Profile_DataBind));
30 model.Select("GridArea", "StudentRepeater", "Schedule", "Summary").SetDataBind(new DataBindHandler(Summary_DataBind));
31 model.Select("GridArea", "StudentRepeater", "Schedule", "CourseRepeater").SetDataBind(new DataBindHandler(CourseRepeater_DataBind));
32
33 model.Select("GridArea", "Pager").HandleAction("NextPage", new ActionHandler(Pager1_NextPage));
34 model.Select("GridArea", "Pager").HandleAction("PrevPage", new ActionHandler(Pager1_PrevPage));
35
36 model.Select("GridArea", "StudentRepeater", "Schedule", "Pager").HandleAction("NextPage", new ActionHandler(Pager2_NextPage));
37 model.Select("GridArea", "StudentRepeater", "Schedule", "Pager").HandleAction("PrevPage", new ActionHandler(Pager2_PrevPage));
38
39 model.HandleAction("OpenPopup1", new ActionHandler(Action_OpenPopup1));
40 }
41
42
43
44 private void Pager1_NextPage(ActionArgs args)
45 {
46 Scopes.ActionPath.Rew(1).Fwd("StudentRepeater").Context.Refresh();
47 Scopes.ActionPath.Context.Refresh();
48 }
49
50 private void Pager1_PrevPage(ActionArgs args)
51 {
52 Scopes.ActionPath.Rew(1).Fwd("StudentRepeater").Context.Refresh();
53 Scopes.ActionPath.Context.Refresh();
54 }
55
56 private void Pager2_NextPage(ActionArgs args)
57 {
58 Scopes.ActionPath.Rew(1).Fwd("CourseRepeater").Context.Refresh();
49 Scopes.ActionPath.Rew(1).Fwd("Summary").Context.Refresh();
60 Scopes.ActionPath.Context.Refresh();
61 }
62
63 private void Pager2_PrevPage(ActionArgs args)
64 {
65 Scopes.ActionPath.Rew(1).Fwd("CourseRepeater").Context.Refresh();
66 Scopes.ActionPath.Rew(1).Fwd("Summary").Context.Refresh();
67 Scopes.ActionPath.Context.Refresh();
68 }
69
70 private void Action_OpenPopup1(ActionArgs args)
71 {
72 Scopes.CurrentPath.Fwd("PopupPlaceholder").Context.Params["StudentSSN"] = (string)args.ActionData;
73
74 Scopes.CurrentPath.Fwd("PopupPlaceholder").Context.Params["ShowDialog"] = "1";
75 Scopes.CurrentPath.Fwd("PopupPlaceholder").Context.Refresh();
76 }
77
78
79
80 private void GridArea_DataBind(DataBindArgs args)
81 {
82 int studentCount = DataFacade.GetStudentCount();
83
84 Scopes.CurrentPath.Fwd("Pager").Context.Params["StartItemIdx"] = "0";
85 Scopes.CurrentPath.Fwd("Pager").Context.Params["PageSize"] = "3";
86 Scopes.CurrentPath.Fwd("Pager").Context.Params["ItemTotalCount"] = studentCount.ToString();
87 }
89
89 private void StudentRepeater_DataBind(DataBindArgs args)
90 {
91 int startItemIdx = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("StartItemIdx");
92 int pageSize = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("PageSize");
93 int itemTotalCount = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("ItemTotalCount");
94
95 object[] students = DataFacade.GetStudents(startItemIdx, pageSize);
96 for (int i = 0; i < students.Length; i++)
97 {
98 args.NewItemBinding();
99 Scopes.CurrentPath.Fwd(i, "Profile").Context.Params.AddRange(students[i], "StudentName", "StudentSSN", "Major", "PhoneNumber");
100 }
101
102 // save id of student repeater scope so that dialog can watch when this scope is refreshed
103 Scopes.CurrentPath.Rew(2).Fwd("Popup2Placeholder").Context.Params["StudentRepeaterID"] =
104 Scopes.CurrentPath.Context.ScopeClientID;
105 }
106
107 private void Profile_DataBind(DataBindArgs args)
108 {
109 int courseCount = DataFacade.GetCourseCount(Scopes.CurrentPath.Context.Params["StudentSSN"]);
110
111 Scopes.CurrentPath.Rew(1).Fwd("Schedule", "Pager").Context.Params["StartItemIdx"] = "0";
112 Scopes.CurrentPath.Rew(1).Fwd("Schedule", "Pager").Context.Params["PageSize"] = "3";
113 Scopes.CurrentPath.Rew(1).Fwd("Schedule", "Pager").Context.Params["ItemTotalCount"] = courseCount.ToString();
114
115 args.NewItemBinding();
116 args.CurrBinder.Replace(Scopes.CurrentPath.Context.Params, "StudentName", "StudentSSN", "Major", "PhoneNumber");
117 args.CurrBinder.Replace("{CourseCount}", courseCount);
118 }
119
120 private void CourseRepeater_DataBind(DataBindArgs args)
121 {
122 string studentSSN = Scopes.CurrentPath.Rew(2).Fwd("Profile").Context.Params["StudentSSN"];
123
124 int startItemIdx = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("StartItemIdx");
125 int pageSize = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("PageSize");
126 int courseCount = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("ItemTotalCount");
127
128 object[] courses = DataFacade.GetCourses(studentSSN, startItemIdx, pageSize);
129 for (int i = 0; i < courses.Length; i++)
130 {
131 args.NewItemBinding();
132 args.CurrBinder.Replace(courses[i]);
133 }
134 }
135
136 private void Summary_DataBind(DataBindArgs args)
137 {
138 int startItemIdx = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("StartItemIdx");
139 int pageSize = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("PageSize");
140 int courseCount = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("ItemTotalCount");
141
142 int fromNum = startItemIdx + 1;
143 int toNum = startItemIdx + pageSize < courseCount ? startItemIdx + pageSize : courseCount;
144
145 args.NewItemBinding();
146 args.CurrBinder.Replace("{FromCourse}", fromNum);
147 args.CurrBinder.Replace("{ToCourse}", toNum);
148 }
149 }
尽管列表5中的代码在我们目前的知识下难以理解,但您应该清楚地识别出我们在第5.1节中讨论过的顶级方法组。SetTemplate()
在第15行实现,SetupModel()
在第20行实现,操作处理程序在第44-70行,其余的是第80-136行中的数据绑定处理程序。
5.3. 实践:将StudentsPage.htm与StudentsPage控制器关联
要将HTML模板与控制器关联,我们必须实现ScopeControl
类(参考图11)的SetTemplate()
方法。这在列表5的第15行完成。SetTemplate()方法有一个类型为ControlTemplate
的参数template
,其图表显示在图12:
图12:ControlTemplate类图
在第17行,我们简单地使用template.Markup
属性将HTML模板的原始内容传递给SF核心。我们从磁盘的物理位置读取StudentsPage.htm文件(参见第4.5节),并将结果赋给Markup
属性。就是这样!想象一下,您可以多么灵活地选择您动态选择的表示模板。
5.4. SF中的数据绑定设计
在控制器中生成数据的过程称为数据绑定,以保持与Forms开发的兼容性。数据绑定是SF站点开发中最复杂的部分。数据是逐个节点地为范围树提供的。为此,开发人员在控制器类中实现一组数据绑定处理程序。一个处理程序负责绑定范围树中的单个范围节点。在页面渲染过程中遍历范围树时,SF会为每个数据范围调用绑定处理程序。处理程序按照后序遍历(post-order walk)和自顶向下(top-to-bottom)的中序遍历(in-order traversal)调用。回想高中时的图表,这实际上称为从右到左的中序遍历,但由于我水平绘制范围树(如图8),我称之为自顶向下。这种遍历顺序显然是这样选择的,因为HTML模板中的实际范围DIV
标签恰好以这种顺序遇到。渲染逻辑与数据绑定处理程序极其简单透明,这就是SF中渲染过程的全部内容!
SF必须明确告知应该为哪些数据范围调用哪些数据绑定处理程序。为此,控制器类中的所有处理程序都必须在开发人员实现的SetupModel()
方法内附加到相应的范围树节点。如图11所示,SetupModel()
方法传入一个类型为ControlModel
的model
变量,其图表显示在图13:
图13:ControlModel类图
因此,类型为ControlModel
的变量model
由开发人员用于选择特定的数据范围并附加数据绑定处理程序。我们为数据绑定特定范围应该执行的典型操作如下:
- 通过将范围路径传递给
Select()
方法来在范围树中选择特定的数据范围。请注意,在SetupModel()
方法内部,范围是在模型范围树中选择的。尝试指定重复轴将导致错误。 - 调用
SelecteNodeSetup
类(参考图13)的SetDataBind()
方法,并传递适当的委托。SelectedNodeSetup类的实例由先前调用的Select()
方法返回——这种设计只是为了允许单行绑定表达式。
典型的SetupModel()
方法中的数据绑定表达式如下所示:
model.Select(<some_scope_path>).SetDataBind(new DataBindHandler(<some_delegate>));
根范围就像范围树中的任何其他范围一样,可以有一个数据绑定处理程序。数据绑定表达式中无需选择根范围,并且可以直接调用SetDataBind()
方法:
model.SetDataBind(new DataBindHandler(<some_delegate>));
5.5. 实践:在PageStudents控制器中附加数据绑定处理程序
列表5中的第27-31行,即PageStudents
控制器,将数据绑定处理程序附加到图8中的模型树的数据范围。第27行将GridArea_DataBind()
函数作为GridArea范围的数据绑定处理程序添加。第30行将Summary_DataBind()
函数作为Summary范围的数据绑定处理程序添加。依此类推。正如在前一节中讨论的,我们必须使用Select()
函数指向特定范围,然后调用SetDataBind()
并传递委托。我们不为根范围进行数据绑定,因为在PageStudents
控制器中没有此需要。
正如我已经提到的,我们必须记住,在SetupModel()
方法内部,我们只处理模型范围树,而不是渲染范围树,因为渲染过程尚未开始。因此,传递给Select()
函数的所有范围路径都不包含重复轴。
最后,我们不必为所有数据范围附加数据绑定处理程序。仅为需要数据来替换占位符或重复内容的处理程序添加处理程序。例如,Schedule范围只是3个其他范围的容器,不包含任何占位符,所以我们实际上不需要将任何数据绑定到它。
5.6. 渲染和控制器执行步骤
图3为我们提供了SF页面执行步骤的顶层概述。我们最感兴趣的是步骤3和4,因为这些是所有“魔法”发生的地方,即执行控制器并渲染页面。现在是时候深入SF核心,仔细看看这些过程了。虽然这次讨论需要一些我们尚未学习到的SF功能知识,但我认为现在谈论渲染过程是更合适的,这样您就可以在我们后续章节中学习SF架构时,对整个渲染过程设计有一个全面的图景。您不必理解这次讨论的每一个细节——随着我们学习后续章节的内容,它们会变得越来越清晰,但您应该对渲染过程以及SF在初始页面加载和异步回发时用于渲染最终输出的活动顺序有一个初步的认识。
所以,我们来看一个典型的用例。最终用户访问“学生申请”网站,目的是获取某个学生及其课表的信息。用户导航到Default.aspx页面,该页面在初始加载时进行渲染。然后,他希望获取下一页的学生信息,并单击分页按钮来分页显示学生记录。这个操作会引发一个到服务器的异步回发。页面的特定部分会被重新渲染,更新的片段会使用部分更新方法在浏览器中刷新。然后,用户会重复操作,导致异步回发,次数不限,直到找到所需信息并关闭浏览器。
以下是SF在执行控制器并为我们的用例渲染结果时所执行的详细活动:
- 初始请求到达Default.aspx页面(参见第3.4节)。
- SF识别出页面需要由控制器/模板对(参见第3.1节)提供服务,并通过调用
ProvideRootControl
事件(参见第3.3节)来检索控制器对象(参见第5.1节)。 - 系统在控制器对象上调用
SetTemplate()
方法(参见第5.1节),以获取与控制器类关联的HTML模板。 - 系统解析HTML模板标记,并根据HTML模板标记中范围
DIV
标签的结构,在内部构建模型范围树数据结构。如果范围DIV
标签格式不正确,则会抛出异常。 - 系统在控制器对象上调用
SetupModel()
方法(参见第5.1节),以使用数据绑定处理程序(参见第5.4节)、操作处理程序(参见第6.3节)和子控制器(参见第5.7节)填充在上一步中构建的模型范围树。- 如果在当前步骤中模型范围树中填充了任何子控制器,则步骤3到5会递归地为每个子控制器重复执行,因此当此过程完成时,我们就拥有一个完整的模型范围树,其中填充了所有子控制器以及数据绑定和操作处理程序。
- 系统在初始页面加载时启动渲染过程。渲染过程仅包含数据绑定过程加上生成的HTML输出。在数据绑定过程中,SF核心按顺序为范围树中的每个数据范围调用数据绑定处理程序(参见第5.4节),从一个NULL范围开始。当树中特定数据范围的绑定处理程序完成时,该范围标记中的占位符将使用处理程序生成的数据进行替换,并且生成的HTML片段将立即添加到最终页面的HTML输出中。结果是,当整个数据绑定过程完成时,我们就拥有一个完全渲染的HTML页面。
- 当渲染过程开始时,模型范围树被视为一个已渲染范围树。根据在绑定处理程序中生成的数据值,数据范围可能会被多次重复。这意味着,为了在数据绑定处理程序中操作已渲染范围树中的范围,我们必须在
Select()
表达式中指定重复轴(参见第4.6节)。
- 当渲染过程开始时,模型范围树被视为一个已渲染范围树。根据在绑定处理程序中生成的数据值,数据范围可能会被多次重复。这意味着,为了在数据绑定处理程序中操作已渲染范围树中的范围,我们必须在
- 在渲染完成后,并在将输出发送给最终用户之前,系统使用
ViewState
机制来持久化已渲染范围树的数据范围的所有范围上下文和参数(参见第5.9节)。 - 页面首次输出给最终用户。
- 用户希望查看下一组学生记录,并单击分页按钮切换到下一页学生。结果是,客户端操作被引发(参见第6.3节),并启动了一个到Default.aspx页面的异步回发。
- 步骤2到5在异步回发时执行的方式与在初始页面加载时完全相同,因此会重新构建完整的模型范围树。
- 在第7步中保存在
ViewState
中的范围上下文被检索,最近的已渲染范围树被恢复。显然,此步骤仅在回发时执行。 - 系统处理引发异步回发到服务器的操作。操作通过其名称和操作源范围的路径来识别(参见第6.1节)。SF查找负责操作源范围的控制器。如果控制器对当前操作具有操作处理程序(参见第6.3节),则调用该处理程序。在操作处理程序内部,开发人员可以访问在前一步恢复的已渲染范围树,并修改数据范围上下文和参数。如果需要,当前控制器可以引发控制器操作,让父控制器处理它(参见第6.4节)。在操作处理程序函数结束时,开发人员必须显式指定已渲染范围树的哪些数据范围需要被刷新(参见第6.5节)。
- 系统在异步回发时启动渲染过程。渲染过程再次包含一个数据绑定过程,该过程逐一调用数据绑定处理程序,但与初始页面加载不同的是,异步回发时的渲染过程不从NULL范围开始。相反,SF从前一步在操作处理程序实现中刷新的数据范围开始重新渲染范围树分支。这意味着只有那些对应于这些重新渲染的范围树分支中数据范围的绑定处理程序才会被调用。**当从某个已刷新范围开始重新渲染范围树分支时,该分支中所有范围的上下文和参数都会被丢弃,因为已渲染范围树的分支是从头开始重建的**。当数据绑定过程完成时,重新渲染的子树产生的输出会被收集并添加到标准的AJAX响应中。
- 在渲染完成后,并在将AJAX响应发送给用户之前,系统再次使用
ViewState
机制来持久化已渲染范围树的数据范围的更新后的范围上下文和参数。在上一歩中更新的已渲染范围树,其结构可能与在第11步中恢复的树不同,因为它的一个或多个分支被重建了。 - 响应返回到浏览器。客户端框架获取部分内容,并将其插入到对应于已刷新数据范围的
DIV
标签中。 - 完成。部分更新完美结束,用户在浏览器窗口中获得更新的页面。此外,步骤9到16会针对不同的操作重复多次,直到用户获得所有所需的学生信息。
整体活动流程应该相当简单透明。请注意,我特意使用了“执行步骤”而不是我们以前在标准ASP.NET Forms中称为“页面生命周期”的术语。这是因为我想强调**SF中不存在页面生命周期**。回想一下,在使用标准ASP.NET Forms中的众多控件时,我们总是不得不关注我们的代码在哪里以及何时执行,这严重影响了代码隐藏类的设计。在SF中,尽管系统本身在页面渲染之前会经历相当多的活动,正如我们刚刚了解到的,但从开发者的角度来看,整个“页面生命周期”归结为简单且严格定义的数据绑定过程!而且,只要模型范围树已知,绑定处理程序的执行顺序以及整个数据绑定过程都是**以数学上的精确性定义的,没有任何歧义的可能性**。
5.7. 子控制器
我们通过处理ScopesManagerControl
的ProvideRootControl
事件(参见第3.4节的列表2)传递给SF的控制器被称为根控制器。根控制器默认负责渲染整个模型树。如果一个控制器负责渲染从模型树中的某个范围开始的子树,那么我说这个控制器被分配给了这个范围。因此,根控制器总是分配给一个NULL范围,并默认渲染整个页面。现在是时候相应地重新定义根范围的术语了。控制器的根范围是该控制器被分配到的范围。根控制器的根范围在模型树中始终是NULL范围。子控制器的根范围通常可以是模型树中的任何范围。
在某些情况下,我们希望页面的某些部分由子控制器渲染。从功能角度来看,SF中的子控制器与ASP.NET中的用户控件或MVC中的部分视图相同。在“学生应用程序”中,最明显的成为子控制器的候选是Pager范围,因为我们不希望为两个完全相同的分页器重复数据绑定逻辑。
将子控制器分配给模型树中的范围类似于附加数据绑定处理程序(参见第5.5节)。控制器在SetupModel()
方法中使用类型为ControlModel
的变量model
作为参数,分两步进行分配:
- 通过将范围路径传递给
Select()
方法,选择范围树中的特定数据范围。 - 调用
SelecteNodeSetup
类的方法SetControl()
,并将控制器实例传递给它。
分配子控制器的典型表达式如下:
model.Select(<some_scope_path>).SetControl(<controller_instance>);
与ASP.NET Forms不同,页面和用户控件继承自不同的父类,SF中的所有控制器都继承自ScopeControl
(参见图11),并且实现方式相同,无论它是根控制器还是子控制器。
注意:在当前实现中,根控制器还有一个额外的要求。与根控制器关联的HTML模板必须包含一个完整的HTML页面,其中包括<html>
、<head>
和<body>
标签。这个要求是由于框架的Alpha版本匆忙且粗糙的实现而设定的,未来很可能会被取消。另外请注意,在当前实现中,除了通过覆盖SetTemplate()
方法将HTML模板与控制器关联之外,子控制器的模板也可以由父控制器提供。我还没有决定这个功能是否需要,但它增加了一些有趣的灵活性,允许通过在父控制器的HTML模板中提供标记来覆盖子控制器HTML模板。
最后,理解**分配给某个数据范围的控制器的单一实例**通过SetControl()
方法来渲染此范围及其子范围,尤其是在**所有重复的树分支**上,这是极其重要的。回想一下,在使用ASP.NET Forms中的重复器或数据网格时,我们可以在数据绑定过程中访问项模板内的实际控件实例。也就是说,标准ASP.NET总是创建可供我们访问的重复控件的物理实例。**在SF中,控制器不是由系统创建的**。开发人员负责实例化控制器并将其实例传递给SetControl()
方法。好的,那么我们如何区分重复范围树分支上的控制器呢?这是通过数据范围上下文机制实现的,我们将在接下来的理论章节中讨论。
5.8. 实践:学生应用程序中的子控制器
“学生应用程序”中分配给模型范围树范围的控制器在图8中显示为围绕相应数据范围的红色背景上的白色标签。白色标签包含分配给数据范围的控制器类的名称。在列表2中,我们已经将PageStudents
控制器分配给根数据范围,因此图8中的模型树中的NULL范围有一个标记为“PageStudents”的红色外框。除了PageStudents
控制器之外,应用程序中还使用了3个控制器。有一个PagerControl
控制器用于分页学生和学生课程,即它被分配给图8中的两个Pager范围。此外,还有PopupControl
和Popup2Control
控制器,分别分配给PopupPlaceholder和Popup2Placeholder范围,这些是用于演示一些高级功能,我稍后会讲到。
查看列表5中PageStudents
控制器的SetupModel()
函数。第22-25行将子控制器分配给特定的范围。首先选择范围,然后对选定的范围调用SetControl()
。为了分页学生记录,第22行将PagerControl
控制器分配给位于StudentRepeater下方的Pager范围(参见图8)。第23行将同一控制器的新实例分配给CourseRepeater下的Pager范围,用于分页学生课程。依此类推。
5.9. 范围上下文
在我们开始进入“学生应用程序”中操作和数据绑定处理程序的主要实践部分之前,我们需要了解范围上下文和参数。已渲染范围树中的每个范围(如图8所示)都有一个关联的上下文。范围上下文用于通过异步回发持久化数据范围的各种参数。可以通过指定所需范围的范围路径来检索范围上下文对象。请注意,上下文**仅在渲染阶段可用,即您只能在操作或数据绑定处理程序中使用它们**。
查看图11的ScopeControl
图。每个继承自ScopeControl
的控制器都有Context
和Scopes
属性。Context
属性用于访问当前控制器被分配到的数据范围的上下文。如果控制器被分配给某个特定范围,那么该范围的上下文就成为该控制器的上下文。也就是说,ScopeControl.Context
只是当前控制器的根范围的上下文的快捷方式。要访问控制器根范围之外的任何范围的上下文,开发人员会在操作和绑定处理程序中使用Scopes
属性。在数据绑定处理程序内部,Scopes.CurrentPath
属性始终指向当前范围,即数据绑定处理程序被调用的范围。例如,如果PageStudents
控制器(参见列表5,第107行)的Profile_DataBind()
绑定处理程序被调用来渲染第3个学生记录的Profile范围,那么Scopes.CurrentPath
就指向“0-GridArea$0-StudentRepeater$2-Profile”范围。Scopes.CurrentPath
属性的类型是ScopePathNavigator
,其图示在图14中给出。
图14:ScopePathNavigator类图
该类的成员如下:
Scopes.CurrentPath.Fwd()
从当前范围向前移动指针。您将相对于当前范围节点的范围路径传递给此函数。如果范围在指定的路径上未找到,则会抛出异常。请注意,由于我们现在处于渲染阶段,范围路径必须包含重复轴。如果重复轴是0,它仍然可以被省略,以便更简洁地指定范围路径。**您只能在由当前控制器服务的已渲染范围树周围移动指针**。您也可以访问子控制器的根范围的上下文,**但不能超出它**。Scopes.CurrentPath.Rew()
将指定数量的段(整数)作为参数,并将指针沿着当前路径向后移动指定的段数。如果指定的步数是0或超过了路径中的段数(范围数),则会抛出异常。**最近回滚的段的轴始终被保留和恢复**,如果调用Rew()
之后立即调用Fwd()
。这是ScopePathNavigator
的一个重要行为,确保我们在向后和向前导航已渲染范围树时始终遵循正确的路径。在接下来的实践示例中,我将演示此行为如何工作。Scopes.CurrentPath.Context
只是获取当前范围的上下文对象。如果当前范围是控制器根范围,则Scopes.CurrentPath.Context
与ScopeControl.Context
相同。
Context
属性的类型是RenderScopeContext
,其图示在图15中。
图15:RendeScopeContext类图
让我们简要解释一下这个类的成员:
Refresh()
函数指示SF从当前范围开始重新渲染范围树。此函数在异步回发时用于实现部分页面更新。ScopeClientID
是在渲染阶段插入到范围DIV
中的ID属性的值。Params
返回ScopeParams
对象,用于操作范围参数。IsVisible
可以设置为FALSE
,指示SF不渲染范围内容。
Params
属性的类型是ScopeParams
,并且在我们的实现中经常使用。此属性允许我们将参数添加到已渲染范围树中的每个范围。`ScopeParams`类的图示在图16中。
图16:ScopeParams类图
该类的成员执行以下操作:
ContainsKey()
函数检查具有特定名称的参数是否存在于集合中。Get()
按名称返回参数(如果未找到则抛出异常)。重载的Get()
函数有一个指定默认值的选项。GetInt()
按名称返回参数并将其转换为整数(如果未找到或无法转换则抛出异常),并提供指定默认值的选项。SetInit()
仅在参数尚未添加到集合中时才添加该参数。AddRange()
接受一个对象和属性名。如果指定了属性名,则使用反射从对象中检索这些属性并添加到参数集合中。如果未指定propertyNames
,则检索对象的所有公共属性并添加到集合中。作为一种选择,AddRange()
可以接受另一个ScopeParams
对象而不是通用对象。这在我们要快速将一个范围的所有或特定参数转移到另一个范围时非常有用。
注意:在Alpha版本中,只允许字符串范围参数,因为我懒得实现序列化。如果您想保存某个通用对象,需要使用XML或JSON格式将其序列化为字符串。在未来的版本中,我们将能够使用任何[Serializable]
或[DataContract]
对象作为范围参数。
最后,让我们对我们在**第5.7节**末尾开始讨论的控制器实例进行更详细的说明。现在我们知道范围上下文是如何工作的,区分例如第一个学生的课程分页器和第二个学生的课程分页器就变得容易了。尽管**控制器实例保持不变,但当前范围的上下文会发生变化**,所以如果我们为第二个学生保存了任何参数,我们现在就可以通过范围上下文检索这些参数。这意味着我们不应该在控制器类中使用成员变量来保存任何参数,因为**对于模型范围树中的相应范围,只有一个控制器实例**。必须改用上下文机制来保存所有在后端逻辑中使用的参数。另外请注意,要在ASP.NET Forms中将参数传递给某个用户控件,我们必须获取用户控件的实例并直接调用其属性或方法。在SF中,我们通过范围上下文将参数传递给控制器。在下一节中,我将提供几个示例,以便您更好地理解上下文。
5.10. 实践:范围上下文
首先,我们来看一个在**图8**上围绕已渲染范围树移动指针的例子。假设当前正在渲染的范围是第3个学生的Profile,并且我们在该范围的数据绑定处理程序内部。`Scopes.CurrentPath`指向“0-GridArea$0-StudentRepeater$2-Profile”,我们希望访问用于分页同一学生记录中课程的Pager范围的上下文。查看**图8**,我们看到要到达课程Pager范围,我们需要向后移动1步,然后向前移动到Schedule和Pager。最终的调用将是:
Scopes.CurrentPath.Rew(1).Fwd("Schedule", "Pager")
这将指针设置为位于“0-GridArea$0-StudentRepeater$2-Schedule$0-Pager”范围路径的Pager数据范围。我们向后回退一步,然后向前移动两步,而没有指定任何轴,但它如何知道**Schedule**范围的轴应该是2
呢?回想一下**第5.9节**,**Profile**范围的轴被保留并自动添加到路径中,除非我们用整数显式覆盖它。这很好,因为我们不一定知道,实际上我们也不想知道当前正在渲染哪个学生记录,我们只是想在同一学生记录中获取Pager,所以段轴的保留允许我们做到这一点。
另一个例子。再次假设当前渲染的范围是第3个学生的Profile,但现在出于某种原因,我想访问第2个学生记录中Pager范围的上下文。表达式将是:
Scopes.CurrentPath.Rew(1).Fwd(1, "Schedule", "Pager")
由于当前路径是“0-GridArea$0-StudentRepeater$2-Profile”,如果我们不显式指定所需的轴,则会保留轴2
。因此,我们只需用轴1
覆盖它,并获得所需的Pager。最终的范围路径是“0-GridArea$0-StudentRepeater$1-Schedule$0-Pager”。
最后,让我们看看重复范围的上下文是如何变化的。在**图8**的已渲染范围树中,学生课程的分页器重复了3次,因为我们有3个不同的学生记录。在列表5的第23行,我们将PagerControl
控制器的实例分配给Pager范围。此控制器的单个实例用于渲染所有3个学生的所有3个范围树分支。当渲染第一个分支时,`PagerControl.Context`是具有“0-GridArea$0-StudentRepeater$0-Schedule$0-Pager”范围路径的Pager范围的上下文。当渲染第二个分支时,`PagerControl.Context`是具有“0-GridArea$0-StudentRepeater$1-Schedule$0-Pager”范围路径的Pager范围的上下文。依此类推。所以控制器实例保持不变,但上下文发生了变化。显然,所有超出控制器根范围的范围的上下文也会发生变化。在ASP.NET Forms中,我们经常将某些数据保存在代码隐藏类中,以便在页面稍后的生命周期阶段使用这些数据。在SF中避免这样做,并将所有值和参数存储在范围上下文中。
5.11. 数据绑定处理程序参数
控制器中的每个数据绑定处理程序都有一个DataBindArgs
类型的参数。**图17**显示了该类的图示。
图17:DataBindArgs类图
NewItemBinding()
方法用于重复范围的子内容。要将内容重复N次,就必须调用NewItemBinding()
N次。如果该方法未在绑定处理程序内部调用,则系统默认假定内容重复一次。CurrBinder
属性必须在每次调用NewItemBinding()
后使用,以告知系统使用哪些数据来替换占位符。
CurrBinder
属性的类型是ItemBinder
,其图示在图18中。
图18:ItemBinder类图
Replace()
方法用于替换实际的占位符。该方法的基本版本接受占位符名称和替换项。将执行简单的查找和替换操作来完成替换。另一种选择是传递一个通用对象和属性名列表。在这种情况下,使用反射根据名称从对象中检索属性,并且**假定占位符是包含在大括号中的属性名**。请注意,这个假设是Alpha版本的一个限制,将来应该会被取消。如果未指定属性名,则检索对象的所有公共属性。最后一个选项是传递ScopeParams
对象。这与前面一个工作方式相同,但我们从集合中检索命名参数,而不是对象的可反射属性。
好了,够了。我认为您已经对如何在数据绑定处理程序中使用函数参数来绑定数据值到占位符有了基本的了解。随着我们继续学习,您会看到更多示例。现在是时候看看实际的数据绑定处理程序了。**要理解这些家伙是如何实现的,我们需要我们迄今为止积累的所有SF知识**。
5.12. 实践:PageStudents控制器的 D数据绑定处理程序
查看图8中的已渲染范围树以及列表5中第80-136行的绑定处理程序。它们将按什么顺序调用,当范围被重复时会发生什么?在前面的章节中,我提到处理程序是使用后序遍历(自底向上)和中序遍历(从左到右)调用的。我在图8的已渲染范围树上可视化了此过程:树节点上的蓝色标签表示对应于数据范围的处理程序的调用顺序。让我们按它们在PageStudents
控制器(**列表5**)中的调用顺序检查所有处理程序。
GridArea_DataBind()
在第80行首先被调用,因为我们没有将任何处理程序附加到根范围。当调用此处理程序时,Scopes.CurrentPath
是“0-GridArea”。在第82行,我们调用数据层来获取学生总数。在第84-86行,您可以看到我们指向Pager范围,向前导航并检索其上下文以向其添加参数。我们在这里所做的只是初始化分页器。"StartItemIdx"
参数初始设置为0
,"PageSize"
设置为3
,"ItemTotalCount"
设置为我们在第82行检索到的学生数量。
接下来要调用的处理程序是第89行的StudentRepeater_DataBind()
。这个更复杂。Scopes.CurrentPath
现在等于“0-GridArea$0-StudentRepeater”。在第91-93行,我们再次访问Pager范围以获取其上下文,但现在我们需要向后退一个段到GridArea,然后向前一个段到Pager。第91-93行用于获取当前分页器值,以便在调用数据层时用于检索学生。您可能会注意到,我们使用了快捷函数GetInt()
(参见图16),而不是获取字符串参数并将其转换为字符串。接下来,在第95行,我们使用分页器值检索实际的学生对象列表。我们通过循环遍历它们(第96-100行),在每次迭代中调用第98行的NewItemBinding()
(参见第5.11节)。调用此方法是为了告诉系统重复范围的内容,即内容重复的次数等于学生数量,这正是我们需要的。接下来,在第99行,我们导航到Profile范围,并通过传递当前学生对象作为数据源来添加一系列参数。我们列出了需要从对象中检索的属性,这些属性将成为参数集合中的参数名称。请注意,我们必须为与i-th学生对应的Profile指定重复轴。请忽略第103-104行——它们是本文结尾更高级讨论的一部分。请注意,我们没有在此范围中执行任何占位符替换。为什么?我们根本不需要,因为StudentRepeater
范围没有任何占位符。
下一个要渲染的范围是Profile,因此在第107行调用Profile_DataBind()
。我们需要学生注册的课程总数,在数据层中有一个函数可以通过学生SSN检索此数字。现在,我们从哪里获取SSN?回想一下,在第99行,循环的i-th迭代为i-th学生向Profile范围添加了一系列参数。假设数组中有3个学生,参数被添加到具有以下路径的范围:“0-GridArea$0-StudentRepeater$0-Profile”、“0-GridArea$0-StudentRepeater$1-Profile”和“0-GridArea$0-StudentRepeater$2-Profile”。当前在处理程序内部的路径是“0-GridArea$0-StudentRepeater$0-Profile”,也就是说,在当前范围的参数中,我们可以找到为第一个学生设置的值(在第一次迭代中)!因此,在第109行,我们只需检索在第99行设置的StudentSSN参数,并使用它来获取课程数量。接下来的3行(111-113)与第84-86行对学生分页器所需的原因相同,即分页器被初始化并添加了一些参数。接下来,在第115行,我们调用方法来指定Profile内容重复一次。第116行使用快捷函数替换当前范围参数中的所有占位符,这些参数是在第99行执行期间设置的。第117行添加了额外的替换以显示课程总数。
下一个范围是Schedule,但由于我们没有附加处理程序,下一个范围变成**CourseRepeater**,并调用CourseRepeater_DataBind()
。当前路径是“0-GridArea$0-StudentRepeater$0-Schedule$0-CourseRepeater”。在第122行,我们回退到Profile,其参数包含所有学生信息,并从中检索学生SSN。请注意,我们再次没有指定任何重复轴,因为回退到**StudentRepeater**两段后,范围导航器保留了前一个节点**Schedule**的轴,然后在我们跳转到**Profile**时使用它。在第124-126行,我们访问课程Pager以获取分页器值。接下来,在第128行,我们使用数据层检索当前页面的课程数组。在第129-133行,我们循环遍历所有课程对象。在第131行,我们告诉系统为每个课程重复**CourseRepeater**的内容,在第132行,我们通过传递整个课程对象来将数据绑定到占位符,这意味着名称等于对象公共属性的可反射的占位符将被相应值替换。
在渲染完学生记录并调用相应的处理程序后,有一个**Pager**范围,但**Pager**范围有一个PagerControl
控制器分配给它(第23行),这意味着该范围及其子范围的数据绑定由PagerControl
控制器而不是PageStudents
控制器处理。在下一节中,我将详细解释PagerControl
;现在,只需假设渲染转到PagerControl
,并且一旦从**Pager**开始的分支被渲染,渲染就会回到我们的PageStudents
控制器。
最后一个范围是Summary,即第136行的Summary_DataBound()
被调用。当前路径是“0-GridArea$0-StudentRepeater$0-Schedule$0-Summary”。在第138-140行,我们再次获取分页器值。然后在第142-143行计算显示的第一门和最后一门课程的数量。在第145-147行,我们绑定占位符以显示所需数据。我刚才说**Summary**是最后一个范围吗?哎呀,我大错特错了,因为在第96-100行,我们重复了学生3次,这意味着代表学生记录的相应子树也得到了重复!反过来,这意味着我们的**Profile**、**CourseRepeater**和**Summary**范围的处理程序会再次以完全相同的顺序为第2个和第3个学生调用!
因此,下一个要调用的处理程序是第107行的Profile_DataBind()
,但这次内部的当前路径是“0-GridArea$0-StudentRepeater$1-Profile”,这意味着我们有了对应于第2个学生的不同上下文!和以前一样,使用StudentSSN检索课程总数,但这个SSN现在是第2个学生的,对应于第96-100行循环的第2次迭代,我们在其中填充了**Profile**范围的参数,也就是说,这次调用数据层返回的是第2个学生的课程数量。依此类推。希望您能理解!:)
在渲染完学生记录并调用相应的处理程序后,我们还没有完成。下一个要渲染的范围是**StudentRepeater**下的**Pager**,但这个**Pager**也有一个分配给它的控制器(第22行),所以这个控制器负责调用自己的处理程序。在下一节中,我们将详细研究PagerControl
控制器。
在学生Pager及其所有子范围渲染完成后,后序遍历会到达**PopupPlaceholder**范围,稍后到达**Popup2Placeholder**。我在这里不深入细节——所有源代码都可用,您可以自己查看。与Pager范围一样,这两个范围也有控制器分配给它们,并且所有渲染逻辑与我们即将要检查的PagerControl
子控制器中的逻辑相同。
5.13. 实践:PagerControl控制器
在本节中,我们将仔细研究PagerControl
控制器。我们在PageStudents
控制器中为两个**Pager**范围(第22、23行)使用了这个控制器,这意味着PagerControl
负责分页学生和学生课程。下面我提供了HTML模板和控制器类的完整列表。HTML模板由Pager.htm文件表示,其列表如下:
1 <div scope="PrevDisabled" style="display:inline"><< Prev</div>
2 <div scope="PrevEnabled" style="display:inline">
3 <a href="javascript:AspNetScopes.Action ('PrevPage', {PrevPageIdx})"><< Prev</a>
4 </div>
5
6 <span style="font-weight:bold;">Page {CurrPageNum} of {TotalPageCount}</span>
7
8 <div scope="NextEnabled" style="display:inline">
9 <a href="javascript:AspNetScopes.Action('NextPage', {NextPageIdx})">Next >></a>
10 </div>
11 <div scope="NextDisabled" style="display:inline">Next >></div>
这段相当简单的标记在浏览器窗口中看起来如下:
模板在同一嵌套级别包含4个范围:**PrevDisabled**、**PrevEnabled**、**NextEnabled**和**NextDisabled**。这个范围结构背后的思想很简单。如果“<< prev”按钮应该启用,则显示**PrevEnabled**并隐藏**PrevDisabled**;否则,显示**PrevDisabled**并隐藏**PrevEnabled**。对于“next >>”按钮也遵循相同的逻辑。
您可能会注意到有趣的JavaScript调用AspNetScopes.Action()
。此API在客户端用于引发在控制器类内部处理的异步操作。在接下来的章节中,我们将详细讨论SF中的操作。
如上模板的控制器,正如我们已经知道的,是PagerControl
类。该类位于PagerControl.cs文件中,该文件的完整列表如下:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Web;
5 using System.IO;
6 using System.Web.Hosting;
7
8 using AspNetScopes.Framework;
9
10 /// <summary>
11 /// Summary description for PagerControl
12 /// </summary>
13 public class PagerControl : ScopeControl
14 {
15 public override void SetTemplate(ControlTemplate template)
16 {
17 template.Markup = File.ReadAllText(HostingEnvironment.MapPath("~/App_Data/Templates/Pager.htm"));
18 }
19
20 public override void SetupModel(ControlModel model)
21 {
22 model.SetDataBind(new DataBindHandler(ROOT_DataBind));
23 model.Select("PrevEnabled").SetDataBind(new DataBindHandler(PrevEnabled_DataBind));
24 model.Select("NextEnabled").SetDataBind(new DataBindHandler(NextEnabled_DataBind));
25
26 model.HandleAction("NextPage", new ActionHandler(Action_NextPage));
27 model.HandleAction("PrevPage", new ActionHandler(Action_PrevPage));
28 }
29
30
31
32 private void Action_NextPage(ActionArgs args)
33 {
34 int startItemIdx = Context.Params.GetInt("StartItemIdx");
35 int pageSize = Context.Params.GetInt("PageSize");
36 int itemTotalCount = Context.Params.GetInt("ItemTotalCount");
37
38 if (startItemIdx + pageSize <= itemTotalCount)
39 {
40 startItemIdx = startItemIdx + pageSize;
41 Context.Params["StartItemIdx"] = startItemIdx.ToString();
42
43 Scopes.NotifyAction("NextPage", null);
44 }
45 }
46
47 private void Action_PrevPage(ActionArgs args)
48 {
49 int startItemIdx = Context.Params.GetInt("StartItemIdx");
50 int pageSize = Context.Params.GetInt("PageSize");
51 int itemTotalCount = Context.Params.GetInt("ItemTotalCount");
52
53 if (startItemIdx > 0)
54 {
55 startItemIdx = startItemIdx - pageSize;
56 Context.Params["StartItemIdx"] = startItemIdx.ToString();
57
58 Scopes.NotifyAction("PrevPage", null);
59 }
60 }
61
62
63
64 private void ROOT_DataBind(DataBindArgs args)
65 {
66 int startItemIdx = Scopes.CurrentPath.Context.Params.GetInt("StartItemIdx");
67 int pageSize = Scopes.CurrentPath.Context.Params.GetInt("PageSize");
68 int itemCount = Scopes.CurrentPath.Context.Params.GetInt("ItemTotalCount");
69
70 int currPage = startItemIdx / pageSize + (startItemIdx % pageSize == 0 ? 0 : 1); ;
71 int pageCount = itemCount / pageSize + (itemCount % pageSize == 0 ? 0 : 1);
72
73 args.NewItemBinding();
74 args.CurrBinder.Replace("{PrevPageIdx}", currPage > 0 ? currPage - 1 : 0);
75 args.CurrBinder.Replace("{NextPageIdx}", currPage + 1);
76 args.CurrBinder.Replace("{CurrPageNum}", currPage + 1);
77 args.CurrBinder.Replace("{TotalPageCount}", pageCount);
78
79 Scopes.CurrentPath.Fwd("PrevEnabled").Context.Params["PrevPageIdx"] = (currPage > 0 ? currPage - 1 : 0).ToString();
80 Scopes.CurrentPath.Fwd("NextEnabled").Context.Params["NextPageIdx"] = (currPage + 1).ToString();
81
82
83 if (currPage > 0)
84 {
85 Scopes.CurrentPath.Fwd("PrevEnabled").Context.IsVisible = true;
86 Scopes.CurrentPath.Fwd("PrevDisabled").Context.IsVisible = false;
87 }
88 else
89 {
90 Scopes.CurrentPath.Fwd("PrevEnabled").Context.IsVisible = false;
91 Scopes.CurrentPath.Fwd("PrevDisabled").Context.IsVisible = true;
92 }
93
94 if (currPage < pageCount - 1)
95 {
96 Scopes.CurrentPath.Fwd("NextEnabled").Context.IsVisible = true;
97 Scopes.CurrentPath.Fwd("NextDisabled").Context.IsVisible = false;
98 }
99 else
100 {
101 Scopes.CurrentPath.Fwd("NextEnabled").Context.IsVisible = false;
102 Scopes.CurrentPath.Fwd("NextDisabled").Context.IsVisible = true;
103 }
104 }
105
106 private void PrevEnabled_DataBind(DataBindArgs args)
107 {
108 args.NewItemBinding();
109 args.CurrBinder.Replace(Scopes.CurrentPath.Context.Params);
110 }
111
112 private void NextEnabled_DataBind(DataBindArgs args)
113 {
114 args.NewItemBinding();
115 args.CurrBinder.Replace(Scopes.CurrentPath.Context.Params);
116 }
117 }
与PageStudents
一样,PagerControl
控制器实现了抽象方法,并在类的主体内包含操作和数据绑定处理程序。在第15行的SetTemplate()
中,我们将控制器与Pager.htm模板关联起来。在第20行的SetupModel()
中,我们附加了多个数据绑定处理程序并添加了操作处理程序。请注意,这次我们将数据绑定处理程序附加到了第22行的控制器根范围。
现在让我们看看PagerControl
中的绑定处理程序。您应该还记得前面关于PageStudents
控制器的讨论,**Pager**是应该在**CourseRepeater**之后进行数据绑定的范围,但由于**Pager**范围有一个分配给它的PagerControl
控制器,因此所有绑定处理程序都在该控制器类内部被调用,而不是在PageStudents
类中。现在让我们看看PagerControl
类,揭示**Pager**范围及其子项是如何进行数据绑定的。
因此,在PageStudents
控制器(参见**列表5**,第120行)中调用CourseRepeater_DataBind()
之后,控制权转移到PagerControl
控制器,这里首先调用的处理程序显然是第二个**Pager**范围(**图8**)的控制器根范围的数据绑定处理程序。**PageControl**中的根范围在**列表7**的第22行附加了ROOT_DataBind()
处理程序。因此,调用ROOT_DataBind()
(第64行)。假设我们已经在渲染第三个学生的迭代中。那么当前范围路径是“0-GridArea$0-StudentRepeater$2-Schedule$0-Pager”。在第66-68行,我们从根范围上下文中获取分页器值。这些参数是如何进入其中的?回想一下PageStudents
类中Profile_DataBind()
方法(第111-113行),在那里初始化了分页器并向**Pager**范围添加了一些参数。这些就是我们在**列表7**的第66-68行检索到的值!因此,我们在PageStudents
控制器中所做的,**我们实际上通过向分配给PagerControl
的Pager范围添加参数,将参数传递给了其子PagerControl
控制器**。另外请注意,每个控制器都有一个上下文,它与其根范围的上下文相同。这意味着我们也可以使用Context
属性而不是Scopes.CurrentPath.Context
来检索分页器值。接下来,在第70-71行,我们计算更多分页器值。在第73行,调用方法来重复分页器内容一次,第74-77行将实际值绑定到模板中的占位符。在第79行,我们将“PrevPageIdx”参数添加到**PrevEnabled**范围,以便稍后在**PrevEnabled**范围进行数据绑定时使用。在第79行对**NextEnabled**范围也做了类似的事情。第83-92行用于启用或禁用“<< prev”按钮。如前所述,当页面不是第一页时,则显示**PrevEnabled**范围并隐藏**PrevDisabled**范围;否则,反之亦然,隐藏**PrevEnabled**,并显示**PrevDisabled**。要隐藏或显示范围,请使用Context.IsVisible
属性。
由于**PrevDisabled**范围只包含一些静态文本并且没有进行数据绑定,因此下一个要调用的绑定处理程序是第106行的PrevEnabled_DataBind()
。当前路径是“0-GridArea$0-StudentRepeater$2-Schedule$0-Pager$0-PrevEnabled”。这里一切都很简单。重复一次内容(第108行),并用上一步`ROOT_DataBind()`处理程序执行期间在第79行填充的当前范围的参数替换占位符。在PagerControl
类中调用的最后一个处理程序是第112行的NextEnabled_DataBind()
,它与前面的PrevEnabled_DataBind()
处理程序完全类似。
现在,当PagerControl
控制器所有处理程序完成后,从**图8**上的第二个**Pager**节点开始的子树就被完全渲染了,控制权被交还给父PageStudents
控制器,该控制器接着调用PageStudents
控制器中**Summary**范围的数据绑定处理程序。
我们完成了数据绑定处理程序,并准备继续学习SF最令人兴奋的功能之一,即处理异步操作和执行部分页面更新的能力。
6. SF操作
6.1. 操作设计
就像ASP.NET Forms中的页面和用户控件可以引发和处理事件一样,SF中的客户端页面可以引发在相应控制器类服务器端处理的操作。在框架的当前实现中,页面上必须有一个ScriptManager
控件,并且由操作引发的所有回发都是异步回发。在Alpha版本中,我懒得实现完整的回发,因为它很简单;但是,我实现了最令人兴奋的部分——带有部分AJAX式分页更新的异步操作!
首先,为了帮助您更好地理解SF中的操作,让我们回顾一下事件在ASP.NET Forms中的工作方式。以下是每次在标准ASP.NET站点中引发和处理事件时发生的顶层活动流程:
- 最终用户通过例如单击某个按钮来操作页面。
- 按钮单击启动表单回发或调用
__doPostBack()
JavaScript函数。如果回发是从更新面板启动的,则__doPostBack()
会被Ajax库版本覆盖,该版本使用XMLHttpRequest
模拟表单回发,而不是进行实际回发。 __doPostBack()
以控件客户端ID作为参数,该ID在__EVENTTARGET
隐藏字段中传输到服务器端。- 使用客户端ID,在页面上找到控件,如果控件有我们事件的处理程序,则调用它。
- 执行其他页面逻辑。
- 页面被渲染并发送回客户端。如果回发是异步的,则不会发送整个页面。相反,会构建一个特殊的分段响应,只包含要刷新的页面部分、视图状态、要注册的脚本等。
- MS Ajax库会解析这个特殊的异步响应,并将更新的内容插入到页面上的相应更新面板中,这些更新面板只是具有已知ID的
DIV
标签。完成!
SF中引发和处理操作的设计很简单,总体流程与ASP.NET Forms的流程非常相似。以下是每次引发和处理操作时发生的顶层活动流程:
- 最终用户通过例如单击某个按钮来操作页面。
- 按钮单击会调用一个特殊的JavaScript函数
AspNetScopes.Action()
,这是SF客户端实现的一部分。 - 该函数的参数是函数所在的范围的路径、操作名称和操作参数。会引发一个异步回发,并将函数参数传输到服务器端。
- SF核心拦截回发。使用范围路径,它在已渲染范围树中定位作为操作源的范围。
- 用于渲染操作源范围的控制器被用于处理操作。系统检查控制器是否为传入的操作名称添加了操作处理程序。
- 如果找到操作处理程序,则调用它。在操作处理程序实现内部,开发人员操作范围及其参数,并**显式指定已渲染范围树的哪些范围需要被刷新**以反映由已处理操作引起的变化。
- 范围树从已刷新的范围开始渲染,而不是从
NULL
范围开始。结果是一组已渲染的树分支,发送回客户端。 - 在客户端,更新的分支被应用于相应的范围,这些范围也是具有已知ID的
DIV
标签。完成!我们的操作已被处理,我们的页面已通过异步回发更新!
您可以看到事件、操作以及在客户端上处理的渲染结果之间明显的相似性。事实上,SF的客户端完全基于标准的MS Ajax库,这使我们能够在保持标准MS Ajax库所有优点的同时,拥有SF的附加功能。
6.2. SF客户端
Alpha版本中SF客户端库的实现比较匆忙,但它确定了我未来为SF正式版本将遵循的实现路径。在现有的MS Ajax实现基础上优雅地构建一些新逻辑的事实,为我节省了大量时间!
如果您在浏览器中查看任何SF页面的源代码,您会看到一个ScriptResourse.axd,它从AspNetScopes.dll程序集中加载AspNetScopes.js文件。该文件包含允许SF引发操作和执行部分页面更新的客户端逻辑。开发人员只需要知道存在AspNetScopes
类,它有几个静态方法可以在客户端执行所有工作。AspNetScopes.Install()
函数由系统调用以进行必要的客户端准备。它与MS Ajax库的Sys.Application.initialize()
函数一起在启动时调用,**并且不打算由开发人员直接使用**。开发人员需要了解以下2个函数:
AspNetScopes.Action(scopePath, actionName, actionArgument)
此函数与标准Ajax库中的__doPostBack()
函数用途相同,即它会引发一个到服务器的异步回发。如果您查看代码(实际上只是一串JavaScript代码),您会发现我们的函数在将范围路径、操作名称和参数保存到隐藏字段后,会调用__doPostBack()
。开发人员需要使用此函数来引发任何操作。AspNetScopes.AddRefreshHander(scopePath, callback)
这是一个辅助函数,您不必使用它。有时,当特定范围被刷新时,需要执行一些JavaScript。这正是这个函数的作用。它接受范围路径和一个回调函数。当范围被刷新时,将在客户端执行回调函数。我将在本文结尾的弹出窗口示例中使用它来执行延迟加载效果。
6.3. 触发和处理HTML模板操作
要从客户端引发操作,开发人员必须在HTML模板中使用AspNetScopes.Action()
函数。**此函数有3个参数,但在模板内部,开发人员只需指定两个**:actionName
和actionArg
。在渲染范围树时,scopePath
参数会根据包含AspNetScopes.Action()
调用的范围自动插入到生成的标记中。
当操作发生并由系统选择相应的控制器进行处理时,SF会搜索为指定的操作名称要调用的操作处理程序。您可能已经猜到,操作处理程序是在控制器类中的SetupModel()
方法中添加的。开发人员必须使用以下表达式来处理操作:
model.HandleAction(<action_name>, <delegate>)
在操作处理程序内部,我们可以像在数据绑定处理程序内部一样操作范围及其参数。回想一下,在绑定处理程序内部,Scopes.CurrentPath
属性始终指向当前正在数据绑定的范围。在操作处理程序内部,Scopes.CurrentPath
属性具有不同的含义,并且始终指向当前控制器的根范围。在操作处理程序内部还有一个可用的属性,即Scopes.ActionPathPath
,它始终指向操作的源范围。**请注意,此属性不能在数据绑定处理程序中使用**。
注意:在未来的实现中,我计划对设计进行一些更改。我们将让Scopes.ControlPath
始终指向控制器的根范围,而Scopes.CurrentPath
在数据绑定处理程序中指向当前绑定的范围,或在操作处理程序中指向操作源范围。这比现在更一致的设计。
6.4. 触发和处理子控制器操作
除了使用AspNetScopes.Action()
在客户端引发操作外,操作还可以由子控制器类引发,并在父控制器中处理。例如,分页控制器处理“NextPage”操作并更新其值,但它也需要通知父控制器已发生操作,以便父控制器可以切换到下一页数据,这种情况非常普遍。
要在控制器类中引发操作,开发人员可以调用Scopes.NotifyAction()
方法,传递操作名称和参数。请注意,**这次参数可以是任何对象**,而不只是字符串,就像从客户端引发操作一样。
操作处理程序在父控制器类中的SetupModel()
方法中添加。要添加处理程序,开发人员必须使用以下表达式:
model.Select(<path>).HandleAction(<delegate>)
处理控制器和HTML模板操作之间的区别在于,对于控制器操作,在调用HandleAction()
并传入操作名称和处理程序函数指针之前,必须通过指向已分配给该控制器的范围来选择实际的子控制器。
6.5. 刷新范围和AJAX
SF最令人兴奋的功能之一是AJAX式部分更新的实现方式。每个典型的Web 2.0页面都以相同的方式工作——页面首次渲染,然后每一次后续用户在该页面上的操作都会导致页面较小部分更新。在ASP.NET Forms中,这种行为是通过更新面板实现的,其主要缺点是异步回发实际上是一个回发,页面仍然会经历整个生命周期,执行大量不必要的代码,即使只需要更新页面的一小部分。
在SF中,实现方式不同。您可能已经猜到,已渲染范围树中的每个范围都是一个更新面板!不,技术上它当然不是一个更新面板,但逻辑上是的,已渲染范围树中的每个范围都可以独立地在异步回发时刷新。**如果范围被刷新,只有从该范围开始的树分支才会被渲染,这意味着系统只为渲染分支中的范围调用数据绑定处理程序**!这难道不美吗?在没有您明确指示的情况下,回发不会执行一行代码。与ASP.NET Forms中渲染整个页面并从中提取更新部分相比,这种过程在性能上非常轻量。
首先,我为什么总是在回发时谈论已渲染范围树?直到渲染过程开始之前,难道我不应该只谈论模型范围树吗?回想一下**第5.6节**中的渲染过程执行步骤。整个页面只在初始请求时渲染,之后我们的模型树就变成了已渲染范围树,并且我们所有的范围上下文都会被持久化。在随后的该页面请求中,会检索持久化的上下文。这些上下文严格定义了先前已渲染范围树的结构。因此,从逻辑上讲,我们总是可以恢复先前输出产生的已渲染范围树。
好的,我们回到部分更新。显然,部分更新仅在回发时才有意义,即作为处理某些操作的结果。因此,操作发生在页面上,回发开始,调用操作处理程序,执行一些代码,然后更新一些作用域(scope) DIV
标签。
系统必须明确告知需要刷新哪个作用域。默认情况下,回发时不会刷新任何作用域。为了告知 SF 刷新作用域,开发人员必须使用树导航指向特定作用域,并在其上下文(context)上调用 Refresh()
函数(参见图 15)。重要的是要理解,作用域只能在操作处理程序内刷新。在数据绑定处理程序中调用 Context.Refresh()
是无效的,因为在渲染过程开始之前,系统显然需要知道已刷新的作用域。
当作用域被刷新时,以该作用域为根的已渲染作用域子树会与该子树中的所有作用域上下文一起被丢弃。然后在渲染过程中,已渲染作用域子树会从被刷新的作用域开始重新构建,即对于从被刷新作用域开始的作用域树(scope tree)分支中的所有作用域,都会在其适用的控制器中调用所有数据绑定处理程序。对于位于已刷新子树之外的作用域,其绑定处理程序不会被调用。这种方法极大地简化了后端逻辑。我甚至不需要提及设计上的好处——对于任何尝试使用 ASP.NET Forms 中的 UpdatePanel 控件构建或多或少复杂的 Web 2.0 UI 的 ASP.NET 开发者来说,这些好处都是显而易见的。
6.6. 实践:客户端的PagerControl操作
回到我们的 PagerControl
。 PagerControl
支持两个操作:“NextPage”和“PrevPage”。当最终用户点击“next >>”按钮(参见图 1)时,会触发“NextPage”操作。当用户点击“<< prev”按钮时,会触发“PrevPage”操作。在本例中,我将仅讨论“NextPage”操作,因为“PrevPage”操作的实现是完全类似的。
请查看列表 6 中的Pager.htm。让我们研究一下 NextEnabled 作用域以及其中的“next >>”按钮。 “next >>”超链接的 href
属性设置为 AspNetScopes.Action()
JavaScript,这意味着在点击“next >>”按钮时会调用此函数。请注意,我们只需要传递 2 个参数,而不是 3 个:操作名称和任何操作参数。在渲染时,另一个代表作用域路径的参数会作为第一个参数插入(参见第 6.3 节)。例如,对于第三个学生,渲染的 HTML 中的调用将如下所示:
<a href="javascript:AspNetScopes.Action('0-GridArea$0-StudentRepeater$2-Schedule$0-Pager$0-NextEnabled', 'NextPage', 1)">Next >></a>
指向 NextEnabled 作用域的完整作用域路径的客户端表示形式作为参数插入。系统使用它来查找“NextPage”事件的相应处理程序。
请看我们如何为操作参数使用 {NextPageIdx} 占位符。对于 NextEnabled 作用域,此占位符在列表 7 的第 115 行使用 NextEnabled 作用域的参数进行绑定,这些参数之前在列表 7 的第 80 行由 NextPageIdx
值填充。这意味着 {NextPageIdx} 占位符始终会被替换为相应的下一页索引,该索引稍后在触发操作时作为操作参数传递到服务器端。
6.7. 实践:服务器端的PagerControl操作
为了处理列表 7 中 PagerControl
内的操作,需要在 SetupModel()
方法中添加操作处理程序。在第 26 行,我们附加了“NextPage”操作的处理程序,在第 27 行,我们为“PrevPage”操作做了同样的事情。
当触发“NextPage”操作时,异步回发会将所有操作信息带到服务器,并调用附加到“NextPage”操作的 Action_NextPage()
处理程序。在第 34-36 行,我们首先检索所有当前的翻页器值。请注意,我们为此目的使用了控制器上下文,但也可以使用 Scopes.CurrentPath.Context
,当在操作处理程序中调用时,它会返回控制器根作用域的上下文。在第 38 行,我们确保下一页确实存在。此检查是多余的,可以省略,因为我们在控制器根作用域的数据绑定处理程序(第 94-103 行)中有一些逻辑,该逻辑会在没有下一页时隐藏可点击的“next >>”按钮。从第 40 行开始,我们进行实际的翻页器参数修改。在第 40 行,我们计算下一页上项的起始索引。然后,在第 41 行,我们用新计算的值更新 StartItemIdx
参数。最后,在第 43 行,我们触发“NextPage”控制器操作,传递 NULL
参数。请注意,我们对客户端和控制器操作都使用了“NextPage”名称,这只是我的偏好,并非必需。另请注意,我们在这里没有刷新任何作用域,尽管参数已被更新。我决定将刷新任务留给拦截该操作的父级 PageStudents
控制器。当触发“PrevPage”操作时,会调用 Action_PrevPage()
处理程序。这里的逻辑与 Action_NextPage()
类似,只是我们的所有计算都是针对上一页进行的。更新完参数后,会以 NULL
参数触发“PrevPage”控制器操作。
实际上,您可能更愿意触发一个“PageChanged”操作,并传递一个指示翻页方向的参数,而不是触发两个不同的控制器操作“NextPage”和“PrevPage”。我们习惯于与许多 ASP.NET Forms 控件一起使用这种行为。但是,对于我们当前的示例,我决定使用两个单独的控制器操作。
6.8. 实践:在PageStudents中处理PagerControl操作
完成我们学生应用程序主要部分的最后一步是在父级 PageStudents
控制器中处理由 PagerControl
触发的控制器操作。为此,第一步是在列表 5 的 PageStudents
类的 SetupModel()
方法中添加操作处理程序。回想一下,我们在模型作用域树中有两个不同的 Pager 作用域:一个用于翻页学生,另一个用于翻页学生课程(参见图 8)。第 33 行处理分配给第一个 Pager 作用域的 PagerControl
的“NextPage”操作。要指向此作用域,会将模型作用域路径传递给 Scopes.Select()
函数。第 34 行处理同一个控制器的“PrevPage”操作。第 36-37 行类似地处理分配给第二个 Pager 的“NextPage”和“PrevPage”操作。
因此,当在学生网格下方点击“next >>”按钮时,会在客户端触发“NextPage”操作,然后 PagerControl
会重新触发“NextPage”操作。在列表 5 的第 33 行添加的 Pager1_NextPage()
处理程序将被执行。您会看到这个处理程序只有两行代码。在第 46 行,我们导航到 StudentRepeater 作用域并对其调用 Refresh()
。这会导致系统从 StudentRepeater 节点开始重新渲染已渲染作用域树,并更新客户端浏览器中页面上的相应作用域 DIV
。在渲染过程中,会检索新的 Pager 值,因为它们已在 PagerControl
的 Action_NextPage()
处理程序(列表 7 的第 41 行)中更新。因此,会在 PageStudents
控制器(列表 5 的第 95 行)的 StudentRepeater_DataBind()
绑定处理程序中检索新一组学生对象,我们的输出会列出不同的学生。但我们还没做完。回想一下,我们没有刷新 PagerControl
内部的任何内容,所以我们应该在这里完成。第 47 行刷新 Pager 作用域,导致以 Pager 作用域为根的已渲染作用域子树再次渲染,并更新页面上的相应 DIV
。
当在学生网格下方点击“<< prev”按钮时,执行第 50 行的 Pager1_PrevPage()
处理程序。处理程序内的代码与 Pager1_NextPage()
内的代码相同,因为我们需要执行完全相同的操作——刷新 StudentRepeater 和 Pager 作用域。
当在学生课程网格下方点击“next >>”或“<< prev”按钮时,会执行 Pager2_NextPage()
或 Pager2_PrevPage()
(列表 7 的第 56 或 63 行)。这里一切都相同,只是 Summary 作用域也受到翻页的影响,需要刷新,所以我们对 Summary 作用域调用额外的 Refresh()
来重新渲染它并更新页面上的内容。
7. 对话框窗口
现在是时候回顾弹出窗口以及它们在学生应用程序中是如何实现的。在这里我们学不到任何新东西;我只是想演示使用 SF 实现一些高级 Web 2.0 UI 功能有多么容易。如今,Web 2.0 最受欢迎的元素之一是在弹出窗口中显示信息,并带有延迟加载效果。要实现弹出窗口,我们通常必须使用第三方脚本,这些脚本在 WWW 上有成百上千种变体。我一直偏爱 jQuery 及其插件,如 jqModal,我们只需创建一个对话框容器,并在选定容器的 jQuery 包装对象上调用 jqModal 初始化方法。然后,可以使用插件提供的 show()
和 hide()
方法来显示或隐藏对话框。
从用户体验的角度来看,延迟加载效果非常有用,因为对话框 UI 不需要等待数据完全加载。对话框窗口可以立即显示,显示一些漂亮的横幅或带有“请稍候...”消息的 AJAX 加载器图标。然后,数据会加载,对话框会使用异步回发进行更新,因此整个过程对最终用户来说是完全无缝的。
正如我在第 1 节中已经提到的,学生应用程序显示两个对话框窗口,它们看起来非常相似,但其底层实现不同。按钮“Popup 1”和“Popup 2”位于每个学生个人资料下方(参见图 1),点击这些按钮会调用对话框,其中包含当前学生的全部信息,类似于学生记录中显示的信息(参见图 2)。我不会深入讨论代码,因为本文篇幅过长,所以请允许我提供一个简要的设计概述。所有源代码都可用,因此凭借您迄今为止的 SF 知识,您将能够浏览项目并理解这些弹出窗口是如何实现的。
7.1. “Popup 1”对话框窗口
两个弹出窗口都实现为子控制器。第一个控制器是 PopupControl
,位于../App_Code/Controls/PopupControl.cs 文件中。与之关联的HTML 模板位于../App_Data/Templates/Popup.htm 文件中。在列表 5 的 PageStudents
控制器中,我们将 PopupControl
控制器分配给我们模型树(图 8)中的 PopupPlaceholder 作用域。
现在,看看Popup.htm 标记。有一个容器 DialogWindow 作用域,它有两个子作用域:ContentView 和 LoaderView。 LoaderView 包含一些标记,用于显示“请稍候...”消息。 ContentView 由 4 个子作用域组成:Profile、Summary、CourseRepeater 和 Pager 作用域。想法是在对话框显示后立即显示 LoaderView。当从数据层加载数据时,隐藏 LoaderView 并显示 ContentView 作用域。
查看列表 73 的 PopupControl
控制器中的 ROOT_DataBind()
绑定处理程序。在第 75 和 76 行,我们从控制器根作用域上下文中分配 showDialog
和 loadedState
变量。在初始加载时,这些值未设置,因此会返回默认值,并且这两个变量都设置为 "0"
。当loadedState 为 "0"
时,在第 78 行将 ContentView 设置为不可见。也就是说,ContentView 作用域在初始加载时始终是不可见的。当 showDialog
为 "0"
时,在第 85 行将容器 DialogWindow 作用域设置为不可见。也就是说,对话框在初始加载时是不可见的。渲染会继续调用 ContentView、Profile 等的绑定处理程序吗?不会!请记住,如果作用域被设置为不可见,那么就没有必要渲染该作用域及其内容,即系统会忽略该作用域及其子作用域的绑定处理程序。这好吗?当然好,因为不必要的代码不会被执行,因为渲染结果无论如何都会被隐藏!
现在看看列表 4 的StudentsPage.htm 模板,在第 50 行。当用户点击学生个人资料下的“Popup 1”按钮时,会在 PageStudents
控制器中触发“OpenPopup1”操作,其参数等于当前学生的社保号。我们使用 {StudentSSN} 占位符将正确的社保号插入到 AspNetScopes.Action()
JavaScript 调用中。在列表 5 的 PageStudents
控制器中,第 39 行,我们为“OpenPopup1”操作附加了 Action_OpenPopup1()
处理程序,因此该处理程序在第 70 行执行。现在看看在这个处理程序中我们做了什么。我们获取在 args.ActionData
中传递的学生社保号,并在第 72 行将其添加到 PopupPlaceholder 作用域的上下文中。请记住,此作用域已分配了我们的 PopupControl
控制器,这意味着我们实际上将 "StudentSSN"
参数传递给了控制器。在第 74 行,我们设置了另一个参数 "ShowDialog"
为 "1"
,所以我们基本上是在告诉控制器显示对话框。最后,我们必须刷新 PopupPlaceholder 以使 SF 重新渲染从该作用域开始的子树。
接下来,树从 PopupPlaceholder 作用域开始重新渲染。这意味着 PopupController
中的 ROOT_DataBind()
(第 73 行)再次被调用。但这次 showDialog
被赋值为 "1"
而不是 "0"
,因为我们在 PageStudents
控制器(第 74 行)中刚刚设置了该值。这意味着这次容器 DialogWindow 作用域在第 86 行变得可见。变量 loadedState
仍然是 "0"
,因此 ContentView 作用域仍然不可见,而 LoaderView 作用域可见。好的,回发完成后,我们在屏幕上看到什么?嗯,由于我们的 DialogWindow 现在可见,并且其内部的 LoaderView 也可见,所以我们看到带有 AJAX 加载器图标和“请稍候...”消息的弹出窗口!这是如何工作的?查看Popup.htm 模板。在 DialogWindow 容器作用域内部,有一个居中的 DIV
,具有固定的定位和 z-index
设置为 3000,以便 DIV
显示在所有其他图层的顶部。我们还有一个用于屏幕覆盖的 DIV
,它占据了当前浏览器窗口的全部大小,提供了模态窗口效果的灰色背景。因此,当 DialogWindow 可见时,其内容就会显示,从而以模态对话框窗口的样式显示居中的 DIV
。
接下来,内容尚未加载。目前您只看到“请稍候...”消息。延迟加载的美妙之处在于您根本不必等待即可获得弹出窗口,并且有意义的内容将在几秒钟内加载。现在查看Popup.htm 模板文件末尾的小段 JavaScript。请记住,我说过“Popup 1”对话框是在没有任何 JavaScript 的情况下编写的?我的意思是它的弹出功能不需要 JavaScript,但我们实际上需要一点 JavaScript 来实现延迟加载。如果没有延迟加载,则不需要任何客户端脚本。那么,那段小脚本的作用是,它使用 AspNetScopes.AddRefreshHander()
(参见第 6.2 节)来设置在指定作用域(由 {CurrScopeID} 占位符指定)刷新时调用的回调。现在查看 PopupControl
的第 89 行,可以看到 {CurrScopeID} 实际上被替换为 PopupControl
控制器根作用域的 ID,即 PopupPlaceholder 作用域。因此,由于我们的 PopupPlaceholder 被刷新,客户端回调被调用。它检查是否存在 LoaderView 作用域,如果存在,它会设置超时以触发“DelayedLoad”操作。LoaderView 存在,因此会触发操作,并在 PopupControl
中由附加在第 38 行的 Action_DelayedLoad()
处理程序处理。
在 Action_DelayedLoad()
处理程序中,我们只需将控制器根 PopupPlaceholder 作用域的 "LoadedState"
参数设置为 "1"
并刷新根作用域。因此,ROOT_DataBind()
再次被调用,但这次 showDialog
和 loadedState
都设置为 "1"
,这意味着 DialogWindow 容器作用域与 ContentView 一起可见,但 LoaderView 被隐藏。请注意,Context.IsVisible
与所有作用域参数一起被持久化,并在作用域或其祖先作用域被刷新时清除,因此我们不必显式设置 Context.IsVisible
为 TRUE
。现在,由于我们的 ContentView 可见,其数据绑定处理程序及其子作用域的数据绑定处理程序会按常规顺序依次在第 92-132 行执行。我不会详细介绍。数据绑定处理程序与我们在列表 5 的 PageStudents
控制器中使用的类似。唯一需要注意的是 PopupControl
控制器中 ContentView_DataBind() 处理程序内的 Thread.Sleep()
调用(第 95 行)。此 Sleep()
模拟了您在从数据库请求数据时通常会遇到的延迟。延迟为 2 秒,这意味着带有“请稍候...”消息的 LoaderView 作用域会显示 2 秒,然后被 ContentView 作用域替换。请注意,现在 LoaderView 不可见,因此Popup.htm 模板末尾的脚本中的回调将找不到 LoaderView,因此,直到 LoaderView 再次显示,否则不会再触发“DelayedLoad”事件。
现在所有内容都已加载,我们可以看到漂亮的对话框。最后要讲的是如何关闭这个对话框窗口。再次查看Popup.htm,可以看到当点击关闭链接时,会触发“ClosePopup”操作,从而执行PopupControl 控制器(第 37 行)中附加的 Action_ClosePopup()
处理程序。在此处理程序内部,我们只需将 "ShowDialog"
参数重置为 "0"
并刷新根作用域。因此,ROOT_DataBind()
再次被调用。 showDialog
为 "0"
,因为我们刚刚将其设置为 "0"
,所以 DialogWindow 容器作用域再次被隐藏。 loadedState
也为 "0"
,因为它在第 81 行被重置,所以下次显示对话框时,它将从 LoaderView 屏幕开始。由于 DialogWindow 再次不可见,当回发完成并且作用域 DIV
标签被更新时,对话框会从屏幕上消失,我们又会看到学生网格。这样我们就回到了初始状态,这意味着我们完成了!
我个人认为,在后端实现一个对话框窗口而无需使用任何第三方脚本是相当酷的,因为代码结构非常简单。当然,后端方法缺少一些炫酷的对话框窗口效果,如过渡或淡入淡出,因为作用域可以设置为可见或不可见,仅此而已。在大多数情况下,我会偏爱带有延迟加载器的后端方法,就像我们刚才看到的,但有时我仍然会使用带有第三方插件的标准方法。“Popup 2”对话框就是使用第三方方法与 SF 结合的例子。
7.2. “Popup 2”对话框窗口
第二个“Popup 2”窗口也实现为控制器。控制器类 Popup2Control
位于../App_Code/Controls/Popup2Control.cs 文件中,并具有与之关联的HTML 模板../App_Data/Templates/Popup2.htm。在列表 5 中,我们将 Popup2Control
控制器分配给模型树(图 8)中的 Popup2Placeholder 作用域。
此控制器以一种快速粗糙的方式实现,只是为了演示与常规 ASP.NET 页面一样混合使用 SF 与第三方 JavaScript 库的能力。我不会详细讨论控制器,只会强调一些重要的时刻。“Popup 2”仍然使用延迟加载方法。其 ContentView、Summary、CourseRepeater 和 Pager 作用域的数据绑定处理程序与前一个 PopupControl
的相同。不同之处在于,我们不再有 DialogWindow 容器作用域——对于当前的对话框实现,我们根本不需要它。请注意,PageStudents.htm(列表 4 的第 82 行)中的“Popup 2”按钮具有 "show-modal-popup2"
CSS 类,如果您查看Popup2.htm 模板末尾的 JavaScript,您会发现该类用于使用 jQuery 选择器附加客户端单击回调。“Popup 2”按钮还有一个 studentSSN
属性,其值是一个占位符,由实际学生社保号替换。当用户点击“Popup 2”按钮时,该 SSN 会在Popup2.htm(第 66 行)的脚本中检索。
其他一切都很简单。点击按钮,执行客户端回调脚本,检索 SSN,使用 jqModal 显示对话框窗口(Popup2.htm 的第 71-73 行),并在第 75 行触发“DelayedLoad”操作。请记住,在服务器端弹出窗口实现中,我们使用了额外的回发来显示对话框,而在这里我们不需要回发,而是立即使用脚本显示对话框。接下来,处理“DelayedLoad”,将 "DisplayStudentInfo"
值设置为 "1"
,刷新根作用域,并在 ROOT_DataBind()
处理程序中将 ContentView 设置为可见。请注意,我们不能隐藏 LoaderView,因为如果我们这样做,那么下次调用对话框时,我们就没有加载器屏幕可以显示了。这对于“Popup 1”对话框来说不是问题,因为如前所述,我们有一个额外的回发来显示对话框,但对于“Popup 2”,我们在显示对话框之前不进行任何回发。还有一点需要提及的是,每次主学生网格切换到另一页时,客户端单击事件必须重新绑定到“Popup 2”按钮,因为 DOM 内容已更改,并且客户端 jQuery 事件绑定已失效。这就是为什么我们在 AspNetScope.AddRefreshHandler()
函数调用中有 {StudentRepeaterID} 占位符的原因。另请注意,此占位符在 Popup2Control
的第 79 行被数据绑定,但其值是从 PageStudents
控制器(列表 5 的第 103-104 行)传递给 Poupu2Control
控制器的。
好了,我想关于学生应用程序就说到这里了。现在我们知道了这个应用程序中的每一行代码是如何工作的。在接下来的最后一节中,我将总结我们的讨论,并尝试概述 SF 的未来开发计划。
8. 总结
在本文的结尾,我想总结一下在 ASP.NET SF 上构建 Web 应用程序的优缺点,分享我对该框架未来发展的想法,并回答您可能遇到的一些潜在问题。
8.1. SF缺点
让我们从缺点开始——我能想到的缺点不多。
- 您可能会想念您喜欢的 Forms 控件,如
Calendar
、TreeView
等。要能在 SF 中使用这些控件,它们必须被重新实现为 SF 控制器和模板。作为一个例子,我将在下一个系列文章和框架开发网站中实现日历控制器,并使用 SF 进行讲解。请关注 SF 的更新。 - 一开始,将页面视为作用域树并使用作用域上下文在数据作用域和控制器之间传递参数可能会有点不习惯。
我对批评完全开放。如果您发现 SF 的任何设计缺陷或问题,请告知我。我们还在设置 SF 开发网站,并设有公共博客用于所有讨论。
8.2. SF优点
对我来说,作为一名每天与 ASP.NET 应用程序和开发者打交道的架构师,SF 的好处是显而易见的。有很多好处——我只列出主要的
- 最大限度地分离表示层和后端。
- 符合 W3C 标准的HTML 模板,仅包含有效的 HTML,允许应用程序的无限 GUI 定制。
- 简单透明的数据绑定逻辑,实现漂亮的后端代码设计。没有页面生命周期。无需在异步回发时进行条件代码执行。
- 基于作用域树的 AJAX,无需额外编码即可实现无限部分更新功能。
- 完全控制表示层。不再有黑盒控件。不再有“如何让这个标签渲染成
DIV
而不是SPAN
或反之?”之类的问题。您从头到尾控制它,没有一个字节会在没有您明确指示的情况下输出给最终用户。 - 与标准 ASP.NET 完全兼容。您可以在一个应用程序中同时拥有 SF 页面和基于 Forms 的页面。
8.3. 未来发展
正如我之前多次提到的,SF 的当前版本是 Alpha 1,仅用于证明基于 HTML 模板的服务器页面概念,这些模板由数据绑定的分层作用域组成。这意味着 Alpha 1 实现缺少许多必须在未来添加的基本功能。现有功能必须清理掉所有快速粗糙的代码,并最终确定。让我们快速讨论一些我遗漏的部分以及还需要做什么。以下列表总结了未来的开发计划。
- 肯定缺少的一点是控制器能够访问
Page
对象。Page
包含许多有用的方法和属性,我们可能在控制器操作中需要它们,例如用于注册客户端脚本、解析 URL、查询字符串和表单参数等方法。我还没有决定是直接访问Page
更好,还是通过某个桥接类只提供某些方法。 - 另一个缺失的部分是能够从标记中将参数传递给控制器。就像我们在 .aspx/.ascx 页面中在标记中指定控件属性一样。我现在的想法是允许在HTML 模板中为数据作用域
DIV
标签指定任何通用属性,但scope
属性除外。这些属性将被转换为同名的作用域参数,并通过作用域上下文机制可用。 - 如何摆脱根控制器的HTML 模板需要完整的 HTML 页面的需求?嗯,这实际上是一个更大的问题。有一个想法是提供一个额外的
ScopeTree
控件,它基于ScopesManagerControl
,可以插入到标准 ASP.NET 页面中的任意数量的实例中。ScopeTree
的渲染方式将与ScopesTreeManager
渲染其输出的方式相同,但它不会将结果插入到页面的<head>
和<body>
的预定义文字中,而是将结果作为自己的内容输出。这是一个极其灵活的解决方案,它将允许开发人员决定是希望使用 SF 来渲染整个页面,还是只渲染其中的一部分。 - 主页面呢?我还没有在当前实现中测试过这些,但我看不出将
head
和body
内容文字放入绑定到母版页的<head>
和<body>
内容占位符的任何问题。当然,对于当前的 Alpha 1 版本,母版页必须使用标准的 ASP.NET Forms 实现,而内容页可以在其中包含ScopesManagerControl
。也就是说,只有内容页可以基于 SF。但是,如果我们有一个我刚才提到的独立ScopeTree
控件,我们可以直接在母版页中使用它,这样母版页也可以基于 SF。因此,有几种选择,而此时我仍在决定最终设计。 - 如果您在浏览器中查看 SF 页面的 HTML 源代码,您会注意到
__VIEWSTATE
字段非常大。这是因为已渲染作用域树的作用域上下文是使用ViewState
持久的,显然,我懒惰地为 Alpha 版本实现了上下文序列化,这意味着系统只是常规地序列化类,保存所有完全限定的类名,浪费了大约 90% 的视图状态数据。在未来的版本中,我将提出一个智能的上下文序列化器。此外,我认为将所有上下文存储在另一个新的隐藏字段中而不是使用ViewState
机制是个好主意。 - 在 Alpha 版本中,作用域上下文对象仅接受字符串作为参数。在未来的版本中,上下文将接受用
[Serializable]
或[DataContract]
标记的通用参数。此外,我认为将永久参数与临时参数分开是有用的。永久参数将像现在一样持久化,而临时参数将仅在单个渲染过程中存在。 - 您可能会对 SF 应用程序本地化有一个疑问。嗯,最简单的方法就是为占位符提供相应的内容。这是最灵活的方式,我们仍然可以使用 ASP.NET.resx 文件。我认为这里不需要其他东西,但我对任何建议都持开放态度。
- 巨大的改进将是**模型作用域树的 VS 设计器支持**!试想一下,您在 Visual Studio 设计器中像作用域树一样构建页面。双击一个节点会生成数据绑定处理程序。等等等等。这样的插件将节省大量时间,消除了用笔和纸计划模型树结构的需要。
- 向前迈出的一步是实现单元测试引擎。当然,控制器类本身就可以在现有单元测试框架下进行完全测试,毫无疑问。但是,在 SF 中,由于HTML 模板的简洁性,我们可以尝试将测试引擎扩展到真正的表示层!在某种高级伪语言中,我们单个单元测试中的活动顺序可能如下所示:显示初始页面→断言输出→模拟某个作用域的某个操作→断言输出→模拟另一个操作→断言输出→依此类推。我想您明白了。我们甚至可以使用骨架 HTML 模板来测试控制器以及示例应用程序的 GUI。
8.4. FAQ
以下是我朋友们经常问我的关于 SF 的一些问题的答案。
- ASP.NET SF 是开源的吗?它肯定会从几个月后发布的第一稳定版本开始成为开源的。
- 哪里是讨论 SF 的最佳地点?开发网站现已启动并运行在 www.rgubarenko.net。它有一个博客,您可以在其中发布所有问题和批评。
- 为什么开发进展如此缓慢?嗯,我正尽我所能地加快进度,但除了 SF,我还有一个年幼的孩子和一份全职工作:) 所以,请耐心等待框架的第一个稳定版本。
- 这个概念会在其他语言上实现吗?已经有一群志愿者要在 PHP 上实现这个概念了,所以我祝他们好运,我们可能很快就会有 PHP SF。我目前不打算做任何除了 ASP.NET SF 以外的事情。但我会在年底前将所有代码、设计、文档和逻辑公开,这样您就可以继续为其他平台实现类似的框架。
结论
好了,我想关于第一篇文章就够了。希望您喜欢它,就像我喜欢开发基于作用域树的服务器页面概念并看到它在实际应用中一样。本文主要不是关于 SF 本身,而是关于一种新的开发模式和方法,它现在正以 ASP.NET 平台的开发框架的形式逐步实现。我相信整个概念值得关注,并在不同平台上的众多实现中拥有未来。
我对任何类型的建议和建设性批评都持开放态度。我已注明我的联系电子邮件,您可以发送您的问题。我会尽力回答所有问题,但请耐心等待,因为我还有很多其他事情要做。我目前正在进行 SF Beta 版本的工作,其中将包含未来开发部分中列出的大部分功能。我还正在创建一个更丰富的 Web 2.0 演示应用程序,其中包含一些高级功能、用户输入、大量操作和一个漂亮的日历控件。讨论这个应用程序的一些高级功能将成为我下一篇文章的主题。
更新(2010年11月23日):我开设了一个公共讨论博客,专门讨论新的Web开发概念和基于它们构建的ASP.NET Scopes Framework。如果您对SF有任何问题或建议,请访问我的博客:www.rgubarenko.net