引言
Apache Tapestry 是一个使用 Java 语言创建 web 应用的面向组件的开发框架。Tapestry 应用建立在根据组件构建的页面的基础上。这个框架能够提供输入验证(input validation)、本地化/国际化(localization/internationalization)、状态/持续性(state/persitency)管理、URL 构建/参数映射(parameter mapping)等功能。
为什么 Tapestry 值得推荐呢?一部分原因是:
- 它便于终端用户使用。Tapestry 在设计最初就考虑到了应用的安全和伸缩性,有内嵌的 Ajax、输入验证、国际化以及异常报告功能。
- 它便于开发人员使用。Tapestry 独一无二的类重加载(class-reloading)特性大大地推动了开发人员的开发效率。借助于 Tapestry,对源代码的修改立马就可以看到结果,不需要重新部署和启动应用 !它的异常报告也极为具体,甚至提供可能的修正建议。
- 它便于 web 设计者使用。Tapesry 页面是有效的 HTML(或 XHTML)文件!你可以用自己喜欢的浏览器打开这些页面。
- 它封装了最佳实践:REST 风格的 URL、可降解的 JavaScript、没有 XML 的配置等等。
- 它支持与 Hibernate 、 Spring 、 Seam 、 Acegi 等框架的集成。
本文中,我们会向大家介绍 Tapestry 框架版本 5 。我们将利用 Tapestry 5 开发一个简单的具有创建/读/更新/删除功能的应用,在创建这个应用的过程中,你将体会到 Tapestry 带来的开发效率的提升。我们会从多方面来讲解 Tapestry 应用,比如应用的页面导航(page navigation)、依赖性(dependency)和资源注入(resource injection)、用户输入验证(input validation)和应用状态管理(state management)等等。你还将了解如何应用 Tapestry 中内嵌的 Ajax 功能来创建自己的支持 Ajax 的组件。
本文的目标在于向大家展示如何借助 Tapestry 在尽可能减小开发花销的情况下创建更漂亮、更好用、更安全、更灵活的应用。
先决条件
开发本文所举例子,需要安装下列软件:
- Java SE Development Kit (JDK) 5.0 版本或更新版本。可以从 http://java.sun.com/javase/downloads/ 下载。
- Servlet 容器,如 Apache Tomcat 5.5 或更新版本。
- 可以选择下载安装 Apache Maven 2.0.8 。在这种情况下,拟不需要再安装一个单独的 servlet 容器,可以使用 Maven 来构建并运行 Tapestry 5 应用(请参考 Appendix 以获得更多相关信息)。
- 我们也建议使用当前流行的集成开发环境(IDE),如 Eclipse 或 NetBeans ,你可以使用这些集成开发环境来编辑应用中的 Java 和 HTML 文件。
第一个 Tapestry 5 应用
开始着手使用 Tapestry 框架来开发应用的方式有很多,其中一种是下载这里提供的 Web archive (WAR) file文件,将它们载入你所选择的 IDE 中。如果你选择的是结合 Web 工具的 Eclipse 的话,那么你需要完成下列步骤:
- 启动 Eclipse 并使用 Java 视图
- 选择“文件”>“导入”……或者在项目浏览窗口右击鼠标,选择“导入”……
- 在“导入”对话框中,选择“WAR 文件”选项,然后点击“下一步”。
- 点击“浏览…”,然后从文件系统中选择WAR 文件。如果你还没有服务器运行环境的话, 那就需要选择一个已安装的运行环境,比如 Apache Tomcat。
- 点击“结束”,IDE 环境会根据导入的WAR 文件生成一个 web 项目。
你也可以使用 Apache Maven ,在 Appendix 中有更多关于如何使用quickstart原型来开发 Tapestry 项目的信息。
在刚创建的这个项目上点击鼠标右键,选择 Run As > Run on Server来启动应用。服务器启动之后,在浏览器地址栏输入 URL: http://localhost:8080/app ,你会看到如下页面:
第一个 Tapestry 应用就这样轻松搞定, 并且启动运行了。我们来看一下这个项目的目录结构:
在 source 文件夹下,你可以找到这个示范应用的 root 包–t5demo。该应用的web.xml部署描述器中,你可以发现一个叫做tapestry.app-package的上下文参数,该参数值就是这个应用包的名字。和几乎所有的 Java web 开发框架不同的是,Tapestry 5 不需要任何 XML 配置文件。刚刚提到的上下文参数是唯一一个你需要提供的配置。它告诉 Tapestry 在运行时从哪里可以找到应用的页面、组件以及其它一些必需的类。比如,页面类应该被存储在tapestry.app-package下名为 pages 的子包中(也就是t5demo.pages),对应地,组件的类则应该存储在t5demo.components中。
第一个 Tapestry 页面
让我们从第一个 Tapestry 页面开始这个示范应用的开发旅程。每个 Tapestry 页面都由一个 Java 类和 Tapestry 组件模板合成。组件模板是指那些以“.tml”为扩展名的文件,这里“tml”是 Tapestry 标记语言(Tapestry Markup Language)的缩写。页面模板和页面通常存储于同一个类包中,但也可以直接存储在 web 应用的根目录(WebContent)中。Tapestry 模板是组织良好的 XML 文档,你可以以修改 XHTML 文档同样的方式来对待这些模板。Tapestry 的元素和属性都在其对应的 XML 命名空间中定义,通常都以“t:”作为前缀。在 Tapestry 中,Start 页面等同于“index”页面,如果 URL 中没有明确指明页面的名字的话,那么 Start 页面会作为默认页面被调用。
针对本文中的示范应用,我们一起来简单地修改一下Start.tml文件。让我们把这个文件中所有的初始内容删除掉,从头开始,使用下面这段代码来更新这个模板:
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <head><br></br> <title>Tapestry 5 Demo Application</title><br></br> </head><br></br> <body><br></br> ${helloWorld}<br></br> </body><br></br></html>
body 标签中有这样一个表达式:${helloWorld},这个表达式被称作扩展(expansion)。在这段示范代码中,这个扩展被用来访问对应的页面类t5demo.pages.Start的helloWorld属性。
接下来,我们在t5demo.pages包中创建一个 Start 类。和几乎所有其他的Java web框架不同的是,Tapestry 不会强迫你在基础类的基础上进行扩展,也不强迫你去实现一个特定的接口。Tapestry 页面是 POJO(Plain Old Java Objects)。和所有 POJO 一样,页面类必须声明为 public,并且必须拥有一个无参构造函数。你需要考虑的仅仅是应用的 root 包和它的页面、组件等等的子包。
这个模板中提到了一个叫做helloWorld的属性,那么我们现在就来创建这个属性。Tapestry 遵循 Sun 的 JavaBeans 代码编写规范,所以我们需要创建一个命名为 **getHelloWorld()的 accessor 方法。但为了更好地理解 Tapestry 的工作原理,我们先不创建这个getHelloWorld()方法,而是创建一个叫做getHello()** 的方法,看看 Tapestry 将会有怎样的反应:
package t5demo.pages;<p> public class Start {</p><p> public String getHello(){</p><br></br> return "Hello World!";<br></br> }<br></br>}
如果这时候你访问这个应用的话,你会看到 Tapestry 的标准异常页面:
这个异常页面,不仅仅拥有报告错误(拼写错误或者错误的属性名)的作用,它还列出一系列有用的属性以方便开发人员纠正其错误。同时,它还指明了错误在模板中所处的位置,它不单告诉你这个错误发生在哪个文件哪行,甚至能够提供给你错误所处位置前后一小段代码。Tapestry 的这个功能比较贴近于开发者的实际需要,也是它本身许多与众不同的优点之一。
现在,我们可以将Start.tml文件中的错误表达式更正为 **${hello}**,然后再刷新页面的话,我们就可以看到这样一个正确的页面:
上面这段简单操作中还蕴藏着一个坚定不移的事实,那就是无论你怎样修改代码,Tapestry 都能像在检索模板中的修改一样轻松检索到 Java 类中的修改。使用 Tapestry,不需要重新部署应用就能查看到代码修改导致的页面和组件类的改变。在这种方式下,你会发现自己的工作效率空前得高,而且没有什么挫折感。这是使用 Tapestry 的另一个好处。在 Tapestry 网站上,你还能查到关于 Tapestry 5 所具有的重加载(reloading)特性的更多信息。
同一个应用的页面通常使用通用的前后一致的导航元素、整体布局、CSS(Cascading Style Sheets)、版权信息等等。传统的方法通常是采用某种服务器端引入(include)的方式,但 Tapestry 的方式是创建并使用一个组件。
第一个 Tapestry 组件
很多 Java Web 框架都使用 SiteMesh 或 Tiles 来修饰页面。在 Tapestry 中,一个简单的组件就可以达到这个目的。我们下面将要创建的第一个组件--Layout--它将会被作为所有页面的统一的页面布局。在这个示范应用中,我们选择 Blue Freedom 设计风格并将它的 markup 复制到t5demo.components代码包下名为 Layout.tml 的新文件中,另外,还有它的 stylesheet 和相关图片也需要复制到这个 web 应用的根目录(WebContent)下。
正如下面的代码段所示,所有页面所需的一致的内容(title,header,footer 等)都需要在这个文件中定义。
元素用来指明布置页面特定内容的位置。在显示页面的时候,这个会由页面特定内容取代。 <html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <head><br></br> <title>Tapestry 5 Demo Application</title><br></br> </head><br></br> <body><br></br> <div class="header"><br></br> <h1>Tapestry 5 Demo</h1><br></br> <h2>Springfield</h2><br></br> </div><br></br> ...<p><strong><t:body/></strong> <div id="footer"></p><br></br> Design by <a href="http://www.minimalistic-design.net">Minimalistic Design</a><br></br> </div><br></br> </body><br></br></html>
这样一来,那些负责页面布局的 HTML 元素完全可以从 Start 页面模板中清除掉,而使用 Layout 组件来引入公用的 HTML 代码。实现这个操作很简单,只要采用在 Tapestry 命名空间中名字和组件类型相吻合的元素就可以了。以这种方式来访问 Layout 组件的话,Start 模板可以简化成如下代码:
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> ${hello}<br></br></t:layout>
注意组件包含 ${hello}的方式:${hello}代表的是页面特定内容,不同的页面含有不同的表单、表格或 web 应用程序其它类型的页面元素。你可能会产生疑问:“究竟应该是 layout 还是 Layout?”回答都是肯定的,因为 Tapestry 实际上不区分大小写。
Tapestry 组件--比如页面--都包含一个组件模板和一个 Java 类,在修改完组建模板之后,现在我们应该来创建类了。你可能已经注意到模板并不指定 stylesheet,但大部分的页面显示风格都是衍生自同一个 stylesheet(和一些相关图片)。那是因为在 Java 类中指定 stylesheet 其实比在模板中要来得容易!Tapestry 专门提供一个注解来达到这个目的:
@IncludeStylesheet("context:style.css")<br></br>public class Layout {<p>}</p>
代码中的“context:”前缀实际上指明了 Tapestry 寻找指定文件的路径。另外一个选项是“classpath:”,用来引用存储在 JAR 中的文件。Tapestry 负责为客户创建一个可以访问所引用文件的 URL。
讲到这里,我们可以跳过那些类似显示“Hello World”的初始应用,直接投入到比较贴近实际的应用中来。我们就从定义模型、定义可显示可编辑的数据对象开始。
自定义模型
我们现在就来为我们的示范应用创建一个简单的域模型。我们即将创建一个 CRUD(Create、Retreive、Update 和 Delete)形式的应用来管理用户,每个用户信息如下所示:
要想实现这样的结果,我们需要为应用创建这样一个模块类:
package t5demo.model;<p> public class User {</p><br></br> private long id;<br></br> private String name;<br></br> private String email;<br></br> private Date birthday;<br></br> private Role role = Role.GUEST;<br></br> ...<br></br>}
User 类中的属性 id 专门用来标识用户,在外部存储(比如数据库)中担当主键。其中用户角色(role)是用 enum 类型来描述的:
package t5demo.model;<p> public enum Role {</p><br></br> ADMIN, USER, GUEST<br></br>}
开发人员通常选择持续存储介质--比如数据库--来存储模型类,并通过数据库访问对象(DAO——database access objects)从数据库中读取数据。在下一面的章节中,我们将一起创建一个 DAO 并将它集成到 Tapestry 页面中去。
Tapestry IoC
Tapestry IoC(控制反转-Inversion of Control)是 Tapestry 的一个子框架,它充当对象或者说服务容器的角色。Tapestry 本身包含超过 120 个互相关联的服务,而 Tapestry IoC 这个子框架是为了专门容纳开发者自行开发的专用服务而设计的。你也可以把 IoC 容器想象成主要关注于管理服务对象的生命周期以及将这些服务对象互相关联起来的 EJB 的一个流水线版本。该子框架中用来关联各服务的部分叫作依赖注射(Dependency Injection-DI)。
Tapestry IoC 框架设计的灵感来自于一些众所周知的容器(如 Spring 、 Guice 和 Apache HiveMind )的特性,它将简单、灵活、易于部署、快速启动和超强功能等优点集于一身。
看到上面列出的这些容器,你可能会因此产生疑问:为什么不直接使用 Spring 或 Guice 呢?为什么要重新开发一个框架?实际上,Tapestry 曾经收到关于生命周期和扩展性方面的一些需求,就这两个方面来说,目前现存的其它容器的表现不太令人满意,而这些特别的需求正是 Tapestry IoC 框架存在的理由。另外,Tapestry IoC 可以单独使用,不需要总是捆绑着 Tapestry。关于 Tapestry IoC 更详细的信息,请参阅 Tapestry IoC 文档。
DAO(Data Access Object--数据访问对象)是从外部资源--例如数据库--访问数据的标准模式。在示范应用中,我们将采用 UserDAO 服务来读取 User 实例。这里需要指出的是,Tapestry IoC 和 Guice 称之为“服务”,但在 Spring 中使用 bean 一词。
public interface UserDAO {<br></br> List<user> findAllUsers();<br></br> User find(long id);<br></br> void save(User user);<br></br> void delete(User user);<br></br> User findUserByName(String name);<br></br>} </user>
正如上面这段代码所示,该接口拥有一个简单的 DAO 所需的所有方法。由于 DAO 的编码实现并不特别,跟本文关系不大,所以这里就不再多费口舌。但在实际编写应用的时候,你还需要一个对象-关系(Object-Relational)映射工具(例如 Hibernate ),当然可能性更大的是选择 Tapestry 5 Hibernate 集成。本例中,我们将简单地使用 java.util.ArrayList 来模拟数据库,避免其它任何偏离本文主旨的配置。
现在,我们需要告诉 Tapestry 在 UserDAO 服务被调用的时候应该实例化哪个实现。出于这个目的,我们需要修改 t5demo.services 应用服务包中的 AppModule 类。如果你还记得的话,在上文讨论目录结构的时候,我们曾提到 t5demo 是该示范应用的根类包,services 这个子包应该专门用于应用类,而 AppModule 类则是定义该应用特定服务的地方,也是配置服务并将服务内建到 Tapestry 的地方。在 AppModule 类中,我们还需要将 UserDAO 服务的接口和它的实现捆绑起来:
public class AppModule {<br></br> public static void bind(ServiceBinder binder) {<br></br> binder.bind(UserDAO.class, UserDAOImpl.class);<br></br> }<br></br>}
一举搞定,上面这段代码中的 ServiceBinder 会将 UserDAO 接口捆绑到 UserDAOImpl 实现上。UserDAOImpl 类有一个默认构造函数,UserDAOImpl 类的实例化能够通过 Java Reflection API 实现。Tapestry 5 会创建一个 UserDAOImpl 的新实例并在页面或服务调用 UserDAO 的时候注射该实例。这一系列的动作都不需要任何 XML 配置。为调用 UserDAO 的类编写单元测试也相当简单。由于所有服务默认的实现模式是singletons,因此只可能创建一个 UserDAOImpl 实例,多线程间或应用的多个用户之间共享这个唯一的实例。
此外,我们还需要在“假数据库”中预备一些数据以供使用。因此,我们需要在 UserDAOImpl 中添加一个初始化函数,并在构造函数中调用此初始化函数。
public class UserDAOImpl implements UserDAO {<p> public UserDAOImpl() { createDemoData(); }</p><p> public void createDemoData() {</p><br></br> save(new User("Homer Simpson", "homer@springfield.org", new Date()));<br></br> save(new User("Marge Simpson", "marge@springfield.org", new Date()));<br></br> save(new User("Bart Simpson", "bart@springfield.org", new Date()));<br></br> ...<br></br> }<p> ....</p><br></br>}
这下就该显示用户信息了。
网格组件
在我们准备创建的第一个页面中,我们将使用表格的形式来显示 Users 列表的内容:
创建这个新版本的 Start 页面需要结合下列四个元素:
- 统一风格的 Layout 组件
- Tapestry 中用来处理表格元素的 Grid 组件
- 访问 Users 列表的 UserDAO 服务
- 集成其它所有元素的 Start 页面本身
上一章节中,我们描述了 Tapestry 5 IoC 的一些基本概念,而在本章节中,我们将会应用上文 AppModule 所创建的服务。我们只需要声明一个 t5demo.services.UserDAO 类型的私有成员,并为其添加 @Inject 注解,就可以实现 UserDAO 服务的注射。 Tapestry 会自行根据服务类型查找到这个 UserDAO,然后通过 field 将这个 UserDAO 连接到相关页面上。
public class Start{<p> @Inject</p><br></br> private UserDAO userDAO;<p> public List<user>getUsers() {<br></br> return userDAO.findAllUsers();<br></br> }<br></br>}</user></p>
这个页面的模板非常简单,只包含一个 Grid 组件实例。在模板中,必须声明 Grid 的 source 参数才能得到想要在页面上显示的数据。本例的 source 是由 getUsers() 返回的 List。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <t:grid source="users"/><br></br></t:layout>
我们可以看到,表格的每一行代表一个 User。每行数据都来自于一个 User 实例的属性,这些数据根据其本身类型在显示时自动格式化,比如“birthday”一栏中的数据就自行格式化成本地日期。Tapestry 幕后其实还做了一些额外的工作,比如将 Role enum 中的字段转变成更易于用户阅读的格式(例如用“Guest”替代“GUEST”)。除此以外,页面上显示的这个表格还拥有排序功能。假如显示数据源所需的行数超出一定的界限,Grid 会自动添加新页面以显示所有的数据。每页能够显示的行数可以通过设置 Grid’srowsPerPage 参数来限定。
按照缺省,Grid 列的顺序和 User 类中各个 getter 的顺序一致。Grid 的 reorder 参数值的设置可以定制列的顺序,参数值的各个属性名之间必须使用逗号分隔。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <t:grid source="users" <strong>reorder="role,email,name,id,birthday"</strong>/><br></br></t:layout>
定制列顺序之后的 Start 页面显示如下:
id 这样的属性其实对于终端用户来说没有什么意义,所以没有必要显示在页面。这时候就需要使用 Grid 组件中的 exclude 参数,这个参数专门用来指明哪些属性是可以跳过不用显示的。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <t:grid source="users" <strong>exclude="id,email"</strong>/><br></br></t:layout><layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><grid source="users"></grid></layout>
在声明了 exclude 参数之后,我们的 Start 页面修改为如下显示:
本文的下续章节中还会提到 Tapestry 的一个强大组件,可以直接利用 JavaBeans 来生成用户输入表单。为了保持域模型的一致,我们还需要另一种方法来隐藏用户的某些属性,使其不在页面上显示,那就是对那些不应该在用户界面显示的属性加注 @NonVisual 注解。在 Tapestry 中,不仅仅 Grid 能够识别这个元信息,还有其它一些组件也能够识别(请参考本文中 BeanEditForm 的相关章节)。这里,我们仅简单地演示如何隐藏 User 的 id 属性。
public class User {<br></br> ...<br></br> @NonVisual<br></br> public Long getId() { return id; }<br></br>}
除了添加注解之外,Grid 组件的声明也需要改用简单方式:
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <t:grid source="users"/><br></br></t:layout>
这些修改带来的最后显示结果是:
下一步要定制的是添加编辑用户的功能,为此需要创建一个新页面。这会在下文再做讨论,这会儿我们先把它暂时搁置一边。我们首先要做的是添加一个 edit 链接,这牵涉到覆写 Grid 组件显示表格的列的方式。在 User 的 name 属性上耍一点小花招并不是解决问题的根本方法,我们选择的方案是在页面上添加一个指向负责修改用户信息的页面的链接。
添加链接的工作可以由一个特殊的模板元素--来达成,这个元素可以快速地将组件模板的一部分作为参数传递给组件。
元素的 name 决定了需要覆写处理方式的属性,而 name 的命名规则是在属性名的后面加上后缀 Cell(此外,表格中每列的属性名也可以通过添加 Header 前缀进行覆写)。
本例中的元素名为 nameCell,因此,Grid 组件将通过 PageLink 组件来处理表格中包含用户名的列。PageLink 组件显示的 HTML 链接将用户导向在 page 参数中指定的页面,调用这个指定的页面所需的额外信息则在 context 参数中定义。本例中,编辑用户资料的是 Edit 页面,调用该页面时还需要传递一个用户 id 参数。此外,还需要一个属性来存储即将被编辑的用户对象才能在调用编辑页面的时候传递该用户对应的 name 和 id 值。为了实现这个目的,我们需要在 Start 类中添加一个叫做 user 的属性,再把这个新建属性捆绑到 Grid 组件的 row 参数上。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <t:grid source="users" <strong>row="user"</strong>><p><strong><t:parameter name="nameCell"><br></br> <t:pagelink page="edit" context="user.id">${user.name}<<br></br>/t:pagelink><br></br> </t:parameter></strong> </t:grid></p><br></br></t:layout>
单个属性含有的展开式和其它属性表达式的数量没有任何限制,你也可以使用点标识( dotted notaion)来表示一个或多个属性,使用这种表达式的目的在于读取(在下文还会接触到“编辑”)内嵌属性。
我们能够使用这种方法定制任意数量的 Grid 的列。
但千万不要忘记在 Start 类中添加 user 属性:
public class Start {<br></br> ...<strong><br></br> private User user;<br></br> public User getUser() { return user; }<br></br> public void setUser(User user) { this.user = user; }</strong><br></br>}
如果 Edit 页面连起码的一个对应类都没有的话,Tapestry 自然会因为无法知道如何去调用这个 Edit 页面而抛出异常。
public class Edit {<br></br>}
我们可以稍后再补充完整 Edit 页面的内容。
到目前位置,我们得到的完整的 Start 页面显示如下:
假如我们想在页面的表格中添加一个不属于 User 类属性的列的话,那要怎么做呢?比如,我们想添加一栏专门用来删除不想要的 user 记录。这时候就要用到 virtual 属性,但可能会稍微多费一点功夫。
Grid 组件通过先生成一个 BeanModel 来决定如何显示一个 bean,例如本例中的 User 对象。我们可以通过创建和定制这个 BeanModel 来完完全全地控制 Grid 的操作方式。还有一些新对象必须注射到 Start 页面中:
- org.apache.tapestry.services.BeanModelSource-- 负责为特定的 bean 类创建 BeanModel 的服务
- org.apache.tapestry.ComponentResources -- 提供 BeanModelSource 需要的一些框架方面的功能
在创建的 BeanModel 中定义我们“人工添加的”列“delete”。
public class Start {<br></br> ...<strong><br></br> @Inject<br></br> private BeanModelSource beanModelSource;<p> @Inject</p><br></br> private ComponentResources resources;<p> public BeanModel getModel() {</p><br></br> BeanModel model = beanModelSource.create(User.class, false, resources);<br></br> model.add("delete", null);<br></br> return model;<br></br> }</strong><br></br>}
在 Start 页面的模板中,我们还需要修改另外两处:首先要显示地告知 Grid 组件它应该应用哪个模型。其次,为显示这个新添加的 delete 列提供一个定义。
ActionLink 组件可以用来监控什么时候用户作出了删除某个用户记录的指令。在收到这个指令的时候,ActionLink 会触发 Start 页面上的一个事件。通过为该事件函数提供特殊命名来观察这个事件。通常来说,id 为“delete”的组件在被触发了某个 action 的时候会调用名为 onActionFromDelete() 的函数。
但问题是究竟要删除的是哪个用户记录?这时候我再一次需要将用户的 id 作为上下文参数传递,id 的值会作为参数传递给所触发的事件处理函数。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <t:grid source="users" row="user" <strong>model="model"</strong>><br></br> <t:parameter name="nameCell">...</t:parameter><p><strong><t:parameter name="deleteCell"><br></br> <t:actionlink t:id="delete" context="user.id">Delete<<br></br>/t:actionlink><br></br> </t:parameter></strong> </t:grid></p><br></br></t:layout> <layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><grid row="user" source="users"></grid></layout>
Start 类中,我们提供这样一个事件处理函数:
public class Start {<br></br> ...<strong><br></br> void onActionFromDelete(long id) {<br></br> user = userDAO.find(id);<br></br> if (user != null)<br></br> userDAO.delete(user);<br></br> }</strong><br></br>}
事件处理函数没有必要一定是公有的,其实最好的选择是作为整个类包的私有函数(也就是函数不需要显式声明修饰符)。Tapestry 可以调用这样的函数,同时同一个类包中的其它类也能够调用该函数(比如单元测试),但这个函数并不属于该页面类的公共 API。
在所有这些修改之后,Start 页面修改为如下显示,用户从该页面还可以链接到 Edit 页面:
## 导航模式
上文提到,我们需要创建一个能够修改 User 的页面,我们称之为 Edit 页面。在这一小节中,我们将描述怎样从 Start 页面链接到 Edit 页面。出于这个目的,我们首先要做的是在 web 应用的根目录下创建一个叫作 Edit.tml 的模板(之前,我们已经创建了一个 t5demo.pages.Edit 空类)。然后,我们要详细描述的是 Tapestry 5 中的导航逻辑,关于 edit 逻辑会在下面一个小节中讲到。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <h1>Edit/Create Citizen</h1><br></br></t:layout>
由于页面是用来修改用户记录的,所以我们需要访问 UserDAO。注射(改为“注入”?)这个服务的方法和之前一样,只要在页面类中创建一个 UserDAO 类型的私有变量并为该变量加注 @Inject 注解就可以了:
public class Edit {<p> @Inject</p><br></br> private UserDAO userDAO;<br></br> ...<br></br>}
Edit 页面需要在知道 User 的 id 的前提下才能修改相对应的用户记录(Edit 页面需要知道要编辑的 User 的 id)。这类导航模式(通常称为 master/detailW 主 / 从)在开发 web 应用的过程中非常常见。Tapestry 5 通过 REST 形式的页面激活上下文(page activation context)机制显式支持这种导航模式。
REST 是具象状态传输( Representational State Transfer )的缩写,是分布式超媒体系统软件构架的一种风格。它的中心原则是 URI 中标识的资源和通过标准接口来修改用户资源、交换这些资源的表现方式。关于 REST 的优点,你可以参阅 Wikipedia 。在我们的例子中,重要的是看到 REST 鼓励我们将应用状态添加到 URL 中,这正是页面激活上下文起作用的地方。
Tapestry 中的所有页面都支持页面激活上下文。页面激活上下文包含了那些可以在多个请求中保存的页面状态。根据 REST 的样例,页面激活上下文是为了保存页面状态才添加到 URL 中的。这和在 HTTP Session 中存储页面状态非常相似,但后者需要激活的 session,并且页面状态是无法作为书签收藏的。
Tapestry 中,页面本身参与提供页面激活上下文信息的工作,并同样参与对这些激活上下文的处理。这些工作主要通过事件处理方法对 “passivate”和“activate”事件的处理来完成。(这些工作主要通过为 “passivate”和“activate”两个事件提供事件处理方法来完成。)对于“passivate”事件,页面应该提供一个(或一组)可以完全代表它内部状态的值。在 Edit 页面中,这个值就是被编辑的用户记录的 id。一旦页面请求(比如表单的递交)得到处理,页面会通过“activate”事件处理方法返回其之前的激活状态,根据这个返回值页面可以重新回复到先前的显式状态。
因此,我们第一步要做的是添加这两个事件处理函数:
public class Edit {<br></br> ...<br></br><strong> private User user = null;<br></br> private long userId = 0;<p> void onActivate(long id) {</p><br></br> user = userDAO.find(id);<br></br> userId = id;<br></br> }<p> long onPassivate() {</p><br></br> return userId;<br></br> }<br></br></strong>}
onActivate() 方法需要传递参数值是一个 long 类型的页面激活上下文,(在方法 onActivate() 中我们希望用一个 long 类型的变量作为我们的页面激活上下文,)相对地,onPassivate() 方法则返回这个页面激活上下文的值。在激活状态下,Edit 页面通过注射(注入)的 UserDAO 服务从数据库中读取 User 记录并将数据存储在私有变量中。
我们接着再来看一下显示了上下文的页面的 URL,如果你访问这个演示例子的首页的话,你会发现编辑 User 记录所访问的 URL 的形式是这样的: http://localhost:8080/app/edit/3 。考虑到 Tapestry 中所有页面的名字都区分大小写,你可以发现上面这个 URL 显示的是 Edit 页面,该页面所要编辑的 User 记录的 id 是 3。这个 URL 很直接很简单,它是 REST 风格,所以可以作为书签收藏(我们可以将这个 URL 保存在浏览器的收藏夹中,过阵子再打开该链接仍然可以得到与当前同样的页面,但如果状态是存储在 HttpSession 中的话就不可能得到同样的结果了)。
这下我们可以追踪所要修改的 User 记录了,我们下一步要做的是提供一个用户界面,这在 Trapestry 中实在再简单不过了。
BeanEditForm 组件
上一章节中,我们开始动手创建可以添加或修改用户记录的页面,并为此创建了一个能够维护页面状态并且知晓被编辑的用户记录的页面类。和大部分面向组件的 web 框架一样,我们还需要创建一个表单组件和内嵌域。每个模型对象的值都应该与合适的域类型相捆绑(比如短文本使用文本框;boolean 值使用 checkbox;enum 类型的值使用下拉列表等等)。此外,我们还要考虑到输入验证。刚刚提到的这些都是常用方法,但 Tapestry 提供一个更简单的方式来完成这些工作。
首先,要为 user 的私有变量创建一个公有的 getter 方法,这个私有变量在页面通过页面激活上下文来恢复状态的时候得到初始化:
public class Edit {<br></br> ...<strong><br></br> public User getUser() {<br></br> return user;<br></br> }</strong><br></br> ...<br></br>}
这些工作到目前为止没有什么难度,不是吗?但还有更简单的方法,只要在模板中添加下列代码就能搞定:
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <h1>Edit/Create Citizen</h1><strong><br></br> <div id="section"><br></br> <t:beaneditform t:id="form" t:object="user"/><br></br> </div></strong><br></br></t:layout>
添加这些代码的目的在于应用 Tapestry 中一个叫做 BeanEditForm 的组件。这个组件只需要一个参数对象,而这个参数必须与我们想要编辑的对象属性相捆绑。你所要做的就只是实现这个捆绑。如果你现在刷新 Edit 页面的话,会看到页面上显示的是一个自动生成的表单,表单中包含了要编辑的模型类的所有属性。
Tapestry 5 又一次为开发者省去了很多工作。我们现在有了一个实用的用户界面,并且得益于 Grid 组件,这个接口还有很大的定制空间。页面上显示的这个表单处理地很仔细,域标签都对 camel 风格的命名进行了适当的处理(比如“userName”会显示为“User Name”);enum 类型的数据自动采用包含所有选项的下拉框(选项值也自动修改为易读的形式);对于可编辑的日期属性则自动使用日历弹出窗口。 BeanEditFormBean 也识别 @NonVisual 注解,所以 id 同样被略过没有显示在页面上。由于 Grid 和 BeanEditForm 组件都利用 BeanModel,所以两者之间互相兼容。
BeanEditForm 组件非常灵活,能够提供很多定制选项:
- 可以通过简单地重新排列源代码中 getter 的顺序来重新排列自动生成的表单中各个域的顺序,当然也可以通过编写另外的源代码实现。
- 可以通过 @NonVisual 注解或者组件参数中提供的以逗号分隔的属性来隐藏某些特定域。
- 完全可以用定制的代码对特定域采取不同的处理方式,以此来替代自动生成的表单中的某些特殊输入文本域(比如密码域)。
- 可以采用修改 Grid 组件元信息同样的方式来动态修改待编辑 bean 的元信息。
- 可以修改表单中各个域的标签名,当然也可以得到全面的国际化(internationalization)支持。
- 可以向表单中添加按钮或者移除按钮。
在编辑表单样例中要做的第二步是验证。由于我们一直在使用表单自动生成功能,设定验证限制最好的地方是在模型对象中。在这个对象中添加验证限制功能,只需在这个特殊域的访问函数上添加 @Validate 注解。
public class User {<br></br> ...<br></br> @Validate("required")<br></br> public String getName() {<br></br> return name;<br></br> }<p> @Validate("required,regexp")</p><br></br> public String getEmail() {<br></br> return email;<br></br> }<br></br> ...<br></br>}
在我们的例子中,我们只要求用户输入用户名和 email 地址。@Validate 注解可以包含一个用来描述验证的限制规则 string 参数。该参数的语法对于 Tapestry 4 的用户来说应该不会陌生,它最关键的地方在于使用逗号把各个限制条件分隔开来。限制条件的名字可以是required、max、min、maxLength、minLength或者regexp。正规表达式验证需要两个参数:正规表达式本身以及验证失败时页面需要向用户显示的信息。这两个验证参数都很长,而且由于其中包含的特殊字符使得正规表达式验证并不适合成为模板的一部分。但幸运是,Tapestry 页面和应用都可以拥有一个信息目录来存储长而复杂的字符串。建立这个信息目录需要在 t5demo.pages 类包中创建一个叫做 Edit.properties 的文件。在我们这里所举的例子中,表达式的键是 mail-regexp,这个键由需要验证的属性名与验证器名字组成。定义验证失败时所显示的信息的键名是在 mail-regexp 的基础上再添加一个 -message 后缀。
email-regexp=^[a-zA-Z0-9._-]+@[a-zA-Z0-9-]+\.[a-zA-Z.]{2,5}$<br></br> email-regexp-message=Provided email is not valid
Tapestry 中所有表格都默认激活客户端验证。如果你在没有填写所有文本域的情况下递交表单,那么你立马会看到下图黑框中的提示信息:
很明显,客户端的验证有时候和服务器端的验证相重复。假如你禁止 javascript 的话,在你提交表单之后会看到如下的错误警告消息,这个警告消息就是在服务器端验证域值时生成的:
一旦用户从客户端提交了表单,服务器端会选择执行适当的 action。假设我们所提交的表单中没有任何验证错误,那么 BeanEditForm 组件会触发一个“success”(表单提交成功)事件。这时候,我们需要提供一个相应的事件处理函数 onSuccess() (这个函数会被匹配到任何组件触发的“success”事件,但 BeanEditForm 是这儿唯一触发该事件的对象,所以不需要做额外的声明)。接下来,我们将不仅仅执行一个 action(在数据库中更新 User 的数据),还将从编辑用户页面回到 Start 页面。
public class Edit {<br></br> ...<br></br> public Object onSuccess() {<br></br> userDAO.save(user);<p> return Start.class;</p><br></br> }<br></br> ...<br></br>}
Tapestry 采用一种特别的方式来解析事件处理函数所返回的值:将它作为指定回复页面的命令。无论是返回一个页面的实例、或页面类、甚或是页面的名字,Tapestry 都能将用户导向一个恰当的页面。Tapestry 还支持其他类型的返回值(想要了解 Tapestry 所能支持的所有类型的返回值,请参考 Tapestry 文档)。
内嵌在 Tapestry 中的验证是有局限性的,尽管它们无论在客户端还是服务器端都能够执行,但在验证的范围上却很有限。这些验证能做的都只是文法上的验证,局限于验证定义中所提供的字符。比如,我们想要验证一个业务逻辑来确认当 User 名字被修改的时候,修改所输入的新名字在数据库中仍然是唯一的。
在这种情况下,Tapestry 会在“success”事件触发前先触发一个叫做“validateForm”的事件。
public class Edit {<br></br> ...<p> @Component</p><br></br> private BeanEditForm form;<p> public void onValidateForm() {</p><br></br> User anotherUser = userDAO.findUserByName(user.getName());<br></br> if (anotherUser != null && anotherUser.getId() != user.getId()) {<br></br> form.recordError("User with the name '" + user.getName() + "' already exists");<br></br> }<br></br> }<br></br>}
正如上面这段代码,我们可以通过 BeanEditForm.recordError() 方法在程序中添加需要在表单上显示的错误警告消息。
在实现了编辑现存用户记录的功能之后,我们需要再考虑一下如何添加新用户。BeanEditForm 有一个非常漂亮的处理方式:如果被编辑的属性值是 null,那么它会自动创建一个新的相关类的实例,并将这个新的实例返回页面。借用 BeanEditForm 的这个处理方式,我们只要添加一个 user 变量的公有 setter 就可以达到添加用户的目的。在页面激活时,我们需要额外注意的是传递的用户 id 不能为 0。
public void onActivate(long id) {<p><strong>if (id > 0) {</strong> user = userDAO.find(id);</p><br></br> this.userId = id;<p><strong>}</strong>} </p>
现在如果我们将用户的 id 传递给 Edit 页面的话,页面会允许你修改对应的用户记录。但如果 id 没有传递成功或者被传递的 id 是 0 的话,那么 Edit 页面会自动创建一个新的 User 实例。
Tapestry 5 中的 Ajax
Ajax 是 Asynchronous JavaScript and XML(异步 JavaScript 和 XML)的缩写,专门用来创建动态的、对终端用户更为友好的网站和 web 应用。使用 Ajax,web 页面能通过客户和服务器之间的异步数据交换来实现灵活的反应和高互动性。Ajax 很强大,如若想了解更多 Ajax 的相关信息,你可以参考 Wikipedia 。
Tapestry 对 Ajax 的支持建立在当前流行的 Prototype 和 script.aculo.us 的 JavaScript 类库之上。由于这两个类库都被打包囊括在 Tapestry 中,所以在应用这两个类库的时候不需要再为它们做独立的部署或配置。
通常来说,在使用 Ajax 的应用中,页面会结合用户请求来刷新其中的一部分显示。Tapestry 中,ActionLink 组件可以用来触发 Ajax 的 action。我们将通过举例来说明其工作原理。在这个例子中,我们会再一次通过修改 Grid 组件来追加一栏包含浏览用户详细资料链接的信息栏。我们这就一起来添加一个名为“view”的栏来更新 Grid 模型,这个新添加的栏的内容最后会以 ActionLink 的形式显示在页面上。我们首先要做的是在 BeanModel 中手工添加这样一个属性:
public class Start{<br></br> ...<br></br> public BeanModel getModel() {<br></br> BeanModel model = beanModelSource.create(User.class, false, resources);<br></br> model.add("delete", null);<p><strong>model.add("view", null);</strong> return model;</p><br></br> }<br></br>}
其次,应该进一步添加一个 <t:parameter/> 来定义这个 view 属性的显示方式。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <h1>Springfield citizens</h1><br></br> <t:grid source="users" model="model" row="user"><br></br> <t:parameter name="nameCell">...</t:parameter><br></br> <t:parameter name="deleteCell">...</t:parameter><p><strong><t:parameter name="viewCell"><br></br> <t:actionlink t:id="view" zone="viewZone" context="user.id">View</t:actionlink><br></br> </t:parameter> </strong> </t:grid></p><br></br></t:layout> <layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><grid model="model" row="user" source="users"></grid></layout>
正如之前提到的那样,ActionLink 是触发服务器端的动作的一个组件。如果对触发动作不做任何定制的话, 那么动作的触发会导致整个页面的刷新。参数 zone 可以告诉 ActionLink 应该调用 Ajax 来更新页面的某个区域,这个更新区域的用户 id 必须是该 zone 参数的值。Zone 组件标识了页面中动态更新的部分。我们先来初试牛刀创建一个用户 id 为 viewZone 的 Zone。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <h1>Springfield citizens</h1><strong><br></br> <t:zone t:id="viewZone"/></strong><br></br> <t:grid source="users" model="model" row="user">...</t:grid><br></br></t:layout>
页面上由 Zone 更新的内容可以用来定义。从某种形式上来说是具有 free-floating 特性的,可以被注射到组件中,block 中的内容不按照默认方式显示。下面这段代码建立了一个 id 为 userDetails 的 block,这个 block 专门用来显示存储在页面的 detailUser 属性中的一些用户属性。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <h1>Springfield citizens</h1><br></br> <t:zone t:id="viewZone"/><strong><br></br> <t:block id="userDetails"><br></br> <h2>${detailUser.name}</h2><br></br> <ul style="padding-left: 40px;"><br></br> <li>Identifier: ${detailUser.id}</li><br></br> <li>Email: ${detailUser.email}</li><br></br> <li>Birthday: ${detailUser.birthday}</li><br></br> </ul><br></br> </t:block></strong><br></br> <t:grid source="users" model="model" row="user">...</t:grid><br></br></t:layout>
当用户点击 ActionLink view 的时候,应用会触发一个 action 事件,并相应地调用与该事件匹配的 onActionFromView 函数。该函数需要用户 id 作为参数传递,进而在数据库中找到相应的用户记录。从数据库中读取的用户记录会存储到私有属性 detailUser 中,这个属性会在中用到。
正如上文所提到的,action 方法返回值类型决定了服务器回复用户请求的类型。在传统非 Ajax 请求中,action 方法返回值类型通常决定了哪个页面是对用户请求的回复。为了实现对页面的部分回复,我们可以返回一个组件或者一个 block,返回的这个组件或 block 的 markup 会在不刷新整个页面的前提下更新 Zone 内容。出于这个目的,我们将通过 @Inject 注解注射刚刚创建的 block。需要注意的是,属性名属性类要与组件客户 id 及组件类保持一致(不然的话,你需要定制@Inject 注解)。注射以后的block 是onActionFromView 函数的返回值,如果这个函数被触发,那么block 的markup 会被插入到对应的Zone 中而刷新部分页面。
public class Start{<br></br> ...<strong><br></br> @Inject<br></br> private Block userDetails;<p> private User detailUser;</p><p> public User getDetailUser() { return detailUser; }</p><p> Block onActionFromView(long id){</p><br></br> detailUser = userDAO.find(id);<br></br> return userDetails;<br></br> }</strong><br></br>}
我们以实例阐述了 Ajax 最基本的功能--刷新页面的一部分。此类功能在 Tapestry 5 中非常容易实现,你基本上不需要自己再编写任何 JavaScript 代码。Tapestry 5 中内嵌的 JavaScript 类库所提供的功能在各浏览器中都兼容。
创建自己的 ajax 组件
很显然,单一采用页面部分刷新无法完全实现 Web 应用的交互性,我们还需要为应用创建定制的 Ajax 组件。本章节中,我们将结合 script.aculo.us JavaScript 类库中的 Ajax.InPlaceEditor 来创建一个 Tapestry 5 组件。我们可以利用 InPlaceEditor 来编辑页面上的一些文本内容。比如,在鼠标点击特定文本内容的时候,页面自动创建一个以该文本内容为值的输入文本域;用户可以修改文本域中的值,通过提交表单将新值取代文本域中的初始值。
首先,我们采用与 CSS 导入到 Layout 中同样的方式来注射含有 InPlaceEditor 的 JavaScript 文件--添加 @IncludeJavaScriptLibrary 注解。需要指出的是,即使一个页面涉及很多组件,并且这些组件中可能有很多都通过 @IncludeJavaScriptLibrary 注解导入同一个 JavaScript 文件,你仍然无需担心重复导入的发生。Tapestry 对任何资源都只导入一次。
其次,我们还要注射一些组件必需的服务:
- org.apache.tapestry.ComponentResources --在上一章节中已提过。
- org.apache.tapestry.PageRenderSupport --用来修饰页面和组件的显示。
- org.apache.tapestry.services.Request -- HttpServletRequest 的一个封装包
我们需要建立一个包含如下内容的 t5demo.components.InPlaceEditor 类:
@IncludeJavaScriptLibrary("${tapestry.scriptaculous}/controls.js")<br></br> public class InPlaceEditor{<p> private static final String PARAM_NAME = "t:InPlaceEditor";</p><p> @Environmental</p><br></br> private PageRenderSupport pageRenderSupport;<p> @Inject</p><br></br> private ComponentResources resources;<p> @Inject</p><br></br> private Request request;<p>} </p>
接下来要创建的是一个必需的名为 value 的组件参数,该参数与 InPlaceEditor 容器属性相捆绑。
public class InPlaceEditor{<br></br> ...<strong><br></br> @Parameter(required = true)<br></br> private String value;<br></br></strong>...<br></br>}
我们再来编写一个用来显示组件的方法。Tapestry 组件的显示渲染过程能够被划分为几个阶段。在此之前,我们尚且没有谈论过这些不同的阶段。但出于尽量简化的原则,本文不准备展开对所有阶段的讨论,如果你是在想对这个话题有个全面的了解,那么我们推荐你参考阅读 Tapestry 文档。
为了对组件显示进行渲染,我们会插入一个 AfterRender 阶段。这个阶段常常与另一个阶段--BeginRender--结合起来渲染 html 标识,或者修饰组件模板。我们的组件没有标识也没有任何模板,所以只需要一个 AfterRender 阶段就够了。插入这个 AfterRender 阶段还需要实现 afterRender(MarkupWriter) 方法。首先可是试着查找 markup 中元素的名字,如果 markup 中没有定义任何元素名的话,就取。然后为组件分配一个唯一的 id。接下去,打开这个 html 元素并对那些非正式参数进行修饰。所谓的非正式参数指的是由模板内部用户提供而非定制组件所定义的那些参数。本例中唯一的正式参数(定制组件所定义的参数)是 value。一个典型的非正式参数例子是:style=“someCssStyle”。通常,我们并不关心用户提供的 Css 风格,但我们可以把它存储起来在需要修饰组件的时候应用。在编写了 value 之后,关闭刚才打开的 html 元素。最后要做的是--编写创建 Ajax.InPlaceEditor 的 JavaScript 代码。
JavaScript“类”--Ajax.InPlaceEditor 的构造函数需要三个参数:
- 第一个是所要编辑的元素的客户 id。
- 第二个是提交修改值的 URL。
- 第三个是包含一些选项的 JSON 对象。
上节中,我们了解了 ActionLink 组件能够用来触发服务器端的 action。在这个例子中,我们使用 ComponentResources 服务来创建 ActionLink。在这个链接被点击的时候,edit 事件会被触发。这个事件的名字会被用来匹配事件触发时应该调用的对应的方法。被传递到 InPlaceEditor 构造函数的 JSONObject 只包含一个键/值对,这个键/值用来命名包含有 Ajax.InPlaceEditor 提交的值的用户请求参数。需要指出的是,PageRenderSupport 用来在页面中添加 JavaScript 代码。该服务会在页面中添加一个 JavaScript 回滚函数,页面 DOM 加载时会调用这个回滚函数。在得到最后的 HTML 之前,我们目前能看到的尽是函数代码:
void afterRender(MarkupWriter writer){<br></br> String elementName=resources.getElementName();<br></br> if(elementName==null)elementName="span";<p> String clientId = pageRenderSupport.allocateClientId(resources.getId());</p><br></br> writer.element(elementName, "id", clientId);<br></br> resources.renderInformalParameters(writer);<br></br> if (value != null)<br></br> writer.write(value);<br></br> writer.end();<p> JSONObject config = new JSONObject();</p><br></br> config.put("paramName", PARAM_NAME);<p> Link link = resources.createActionLink("edit", false);</p><br></br> pageRenderSupport.addScript("new Ajax.InPlaceEditor('%s', '%s', %s);",<br></br> clientId, link.toAbsoluteURI(), config);<br></br>}
最后要做的是为 action 方法提供一个匹配的触发事件。点击 ActionLink 会触发 edit 事件,那么相应地,所需要添加的 action 方法应该是 onEdit。在这个方法中,我们从特定的请求参数中得到提交的值,并为对应捆绑的属性赋予这个新值。要更新客户端的这个新值,我们向客户端返回一个含有所需数据的 TextStreamResponse 类型的回复。当然还可以选择其它类型作为返回值来更新客户端数据。
- 组件或 block。上一章节中,我们用到了 block 来更新 Zone。
- JSON 对象
Object onEdit(){<br></br> value = request.getParameter(PARAM_NAME);<br></br> return new TextStreamResponse("text", value);<br></br>}
接下来的例子中,我们将一起使用 InPlaceEditor 来创建一个页面。为将要编辑的页面添加一个 edit 属性,再通过 @Persist 注解使得 Tapestry 在用户请求中保留文本域的值,然后为 edit 属性添加公有的 getter/setter 方法。
public class InPlaceEditorExample {<br></br> @Persist<br></br> private String edit="Please click here";<br></br> public String getEdit() { return edit; }<br></br> public void setEdit(String edit) { this.edit = edit; }<br></br>}
在模板中把 value 参数与 edit 属性捆绑:
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"><br></br> <h1>Using InPlaceEditor</h1><br></br> <div t:type="InPlaceEditor" value="edit"/><br></br></t:layout>
最后得到的 html 代码如下:
...<br></br> <div id="inplaceeditor">Please click here</div><br></br> ...<br></br> <script type="text/javascript"><br></br> Tapestry.onDOMLoaded(function() {<br></br> new Ajax.InPlaceEditor('inplaceeditor',<br></br> '/app/inplaceeditorexample.inplaceeditor:edit', <br></br>{"paramName":"t:InPlaceEditor"});<br></br> });<br></br></script>
调用页面 http://localhost:8080/app/inplaceeditorexample 的话,你可以看到 edit 属性的默认值是“Please click here”。
如果点击这行“Please click here”的话,页面会变成如下所示:
在文本框中输入一个新值,然后点击 ok 按钮,edit 属性的值则会由提交的这个新值所替代。当然我们应该为这个组件继续进一步添加一些实用功能,比如可以添加对 edit 文本框的验证等等。但本文的目的在于给大家提供一个关于 Tapestry 功能的全局概览,而不是要开发一个能够用于产品的完备组件包。
总结
我们为大家展示了如何快速并高效地创建一个 Tapestry 应用。我们曾使用 Maven 的 Java NCSS 插件计算应用中非注释的代码行有多少,结果如下:
- 示范应用中包含 205NCSS(non commenting source lines 非注释源码行) Java 代码。
- 页面模板有 56 行,其中存储页面的 Layout.tml 中占了 34 行。
- 两个最大的 Java 类是 UserDAOImpl 和 User,两者加起来总共 68 行。
- 最大的页面类是 t5demo.pages.Start,包含 27Java 代码和 12 行模板代码。借助于 Tapestry 5,我们能够在页面上为用户提供一个可排序可分页并具有删除和查看功能的表格,只用 39 行代码就可以使用 Ajax 来刷新页面的特定部分以及渲染页面显示。
很明显,代码行不是唯一的度量标准。但肯定的是代码越多,越难维护越难以理解和测试。
Tapestry 团队在提高框架开发友好性上下了大量的功夫:
- Tapestry 5 应用几乎不需要任何配置。大部分你平时需要设定配置的地方,Tapestry 5 都已经设定了合理的默认值。你可以发现,我们的例子中没有任何编写任何 XML 代码。
- 独特的类重加载特性推动俄开发效率的提升。无须为了页面或者组件代码中的小 bug 就重启应用容器,页面和组件类能够自动重新加载。
- 面向开发员友好的错误报告不只提示错误行号并显示前后代码段,还提出可能的修正建议。
Tapestry 5 结合了很多应用领域的实用特性,比如受 Ruby on Rails 启发的配置与 scaffolding 间的转换、受 Google Guice 启发的非 XML 依赖性注射和配置、针对伸缩性问题的 REST 类型的 URL 等等。Tapestry 的输出是有效的(X)HTML 代码。诸如 Grid 和 BeanEditForm 那样的复杂组件都可是可访问的、非 table 类型并能够用 CSS 定制。输入的默认编码是 UTF-8,框架本身支持本地化。
本文中,我们只覆盖了 Tapestry 5 新特性的“冰山一角”,还有很多特性比如更深层次的 Tapestry IoC、Tapestry 应用的本地化、mixins 和 assets 都没有能够一一道来。另外,Tapestry 5 针对 Hibernate 、 Spring 、 Seam 、 Acegi 等的集成模块也没有能覆盖到,还有很多特性都没有提到。但通过本文,我们希望你能够体会到 Tapestry 5 下开发是多么地简单,借助于 Tapestry 5 你可以在很短的时间内开发出健壮的成品应用。
致谢
感谢 Tapestry 的创始人 Howard M. Lewis Ship 对本文的审校和最终校读。非常感谢他给我们提供了宝贵的反馈。另外,我们也非常感谢他创造了 Tapestry。最后,我们要感谢整个 Tapestry 团队,还有那些正在开发 Tapestry 周边开源项目的人们。
Quickstart 和示范应用
本文的“Tapestry 入门”和演示应用的源码可以从 Google Code 上的 tapestry4nonbelievers 项目页面下载。这两个应用都已打包到 WAR,下载之后可以直接载入到你所选择的集成开发环境中。
参考
着手创建Tapestry 应用最简单的方式是使用Maven 和Tapestry Quickstart 原型。Maven 原型是一个预先定义的项目模板,可以用来快速创建新项目。Tapestry 的quickstart 原型则定义了项目目录结构、初始元信息以及一些基本的应用类。假如你对Apache Struts 比较熟悉的话,那你应该知道Struts 发布中包含的struts-blank.war 文件,这个文件中包含一个空Struts 应用,但这个应用完全可以部署并能正常运行。
Tapestry 5 Maven 原型拥有一个仅包含一个页面的空 Tapestry 应用,可以直接编译和部署到 servlet 容器中。如果你已经安装了 Maven 只需要执行如下命令:
mvn archetype:create -DarchetypeGroupId=org.apache.tapestry -DarchetypeArtifactId=quickstart \<br></br>-DgroupId=t5demo -DartifactId=<strong>app</strong> -DpackageName=<strong>t5demo</strong> -Dversion=1.0.0-SNAPSHOT
Maven 使用原型创建了名为 app 的 Tapestry 应用。Maven 创建这个 app 子目录来存储新项目。我们来看一下 Maven 为你自动生成了怎样的目录结构。如果你对 Maven 比较熟悉的话,你应该也会觉得这个默认的 Maven 目录结构有些眼熟:
使用 Jetty servlet 容器启动应用,然后转移到新建的 app 目录下执行一下命令:
mvn jetty:run
Maven 则会编译自动生成的应用、下载(仅在第一次使用的时候!)Jetty servlet 容器,然后启动编译好的应用程序。一旦应用被启动,你可以在浏览器中访问这个应用: http://localhost:8080/app 。
不幸的是,Maven 在 Jetty 运行的时候不知道如何重新编译 Java 类。如果想利用 Tapestry 5 的重加载特性的话,你需要所使用的 IDE 配置适当的 servlet 支持。
评论