介绍
Apache Tapestry是用于Java创建Web应用程序的面向组件的框架。 Tapestry应用程序是从组件构建的页面构建的。 该框架负责输入验证,本地化/国际化,状态/持久性管理,URL构造/参数映射等。
为什么要考虑使用Tapestry? 一些原因是:
- 最终用户友好。 Tapestry在设计时考虑了安全性和可伸缩性要求。 内置了Ajax,输入验证,国际化和异常报告。
- 它对开发人员友好。 Tapestry通过独特的类重新加载功能提高了开发人员的生产率。 使用Tapestry,您可以更改源代码并立即查看结果。 无需重新部署,无需重新启动! 异常报告非常详细,甚至包含建议。
- 它对网页设计师友好。 挂毯页面是有效的(X)HTML! 您可以使用自己喜欢的浏览器打开它们!
- 它封装了最佳实践:RESTful URL,可降解JavaScript,无XML配置
- 它与Hibernate , Spring , Seam , Acegi等集成。
在本文中,我们将向您介绍该框架的版本5 。 我们将使用Tapestry 5开发一个简单的Create / Read / Update / Delete应用程序,并展示Tapestry提供的一些生产力优势。 我们将描述Tapestry应用程序的不同方面,例如页面导航,依赖性和资源注入,用户输入验证和应用程序状态管理。 您还将看到如何使用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容器,可以使用它来构建和运行Tapestry 5应用程序(更多信息,请参阅附录 )。
- 我们还建议使用现代集成开发环境(IDE) ,例如Eclipse或NetBeans 。 您可以使用它们来编辑应用程序的Java和HTML文件。
您的第一个Tapestry 5应用程序
使用Tapestry进行开发的方式有很多,其中一种是从此处下载提供的Web存档(WAR)文件并将其导入您选择的IDE中。 如果您将Eclipse与Web Tools一起使用,则需要执行以下操作:
- 启动Eclipse并切换到Java透视图
- 选择“ 文件” >“ 导入...”或在“ 项目资源管理器”中右键单击,然后选择“ 导入...”。
- 在“导入”对话框中,选择选项WAR文件 ,然后单击“ 下一步” 。
- 使用“ 浏览...”按钮从文件系统中选择WAR文件 。 如果尚未完成,请选择已安装的服务器运行时环境,例如Apache Tomcat。
- 单击“ 完成”以从导入的WAR文件创建Web项目 。
您也可以使用Apache Maven ,在附录中,您将找到有关如何使用quickstart原型启动Tapestry项目的详细说明。
要启动该应用程序,请右键单击最近创建的项目,然后选择运行方式 > 在服务器上运行 。 服务器启动后,在浏览器中调用URL http:// localhost:8080 / app 。 结果将如下所示:
在源文件夹中,您将找到应用程序的根软件包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标记语言 。 页面模板存储在与页面类相同的程序包中,或者存储在Web应用程序根文件夹( WebContent )中。 Tapestry模板是格式良好的XML文档。 您可以像编辑XHTML文档一样对其进行编辑。 Tapestry元素和属性在Tapestry XML名称空间中定义,按照约定,使用前缀“ t:”。 在Tapestry中,“ 开始 ”页面等效于“索引”页面; URL不能标识特定页面名称时,将显示该页面。
对于我们的示例,让我们编辑文件Start.tml 。 让我们丢弃该文件的初始内容,然后重新开始。 使用以下内容更新模板:
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Tapestry 5 Demo Application</title>
</head>
<body>
${helloWorld}
</body>
</html>
在body标签内,您可以看到表达式$ {helloWorld} 。 这称为扩展 。 在这种情况下,扩展用于访问页面类t5demo.pages.Start的属性helloWorld 。
现在,在包t5demo.pages中创建Start类。 与几乎所有Java Web框架不同,Tapestry不会强迫您从基类扩展您的类或实现特定的接口。 挂毯页面是POJO(普通的旧Java对象)。 像任何其他POJO一样,页面类必须是公共的,并且必须具有无参数的构造函数。 您唯一需要考虑的问题是应用程序的根包及其用于页面,组件等的子包。
该模板引用了一个名为helloWorld的属性,因此让我们创建它。 Tapestry遵循Sun的JavaBeans编码规范,因此我们需要创建一个名为getHelloWorld()的访问器方法。
为了演示,让我们创建它,而是创建一个名为getHello()的方法,并查看Tapestry的React:
package t5demo.pages;
public class Start {
public String getHello(){
return "Hello World!";
}
}
运行应用程序时,您应该看到标准的Tapestry例外页面:
“例外”页面不仅报告问题(拼写错误或其他错误的属性名称),而且还通过提供可用属性列表提供了一些解决方法。 它还指出了模板中错误的发生位置,不仅列出了文件名和行号,甚至还为您显示了简短的摘录。 这种对实际开发人员需求的关注是Tapestry真正与众不同的领域之一。
现在我们可以更改Start.tml以将扩展名更改为$ {hello} ; 当我们在浏览器中刷新页面时,将看到工作页面:
这是一个有趣而强大的事实:您可以改为更改代码,而Tapestry可以像对模板所做的更改一样容易地对Java类进行更改 。 Tapestry不会强迫您重新部署,只是为了查看页面和组件类的更改。 这样,您将发现自己以前所未有的高生产率和低挫败感工作。 使用Tapestry的另一个好处。 Tapestry网站上提供了有关Tapestry 5实时重新加载功能的更多信息。
通常,应用程序的页面将具有用于导航,常规布局,使用CSS(层叠样式表),版权消息等的公共且一致的元素。 传统的方法是使用某种形式的服务器端包含,但这不是Tapestry的方式……Tapestry的方式是创建和使用组件。
您的第一个Tapestry组件
许多Java Web Framework使用SiteMesh或Tiles进行页面装饰。 在Tapestry中,这可以通过一个简单的组件来完成。 我们的所有页面都将使用我们的第一个组件Layout来包装特定于页面特定内容的通用布局。 我们为演示应用程序选择Blue Freedom设计,并将其标记复制到位于软件包t5demo.components中的新文件Layout.tml中。 我们还将其样式表和相关图像复制到Web应用程序的根文件夹( WebContent )中。
如下面的示例所示,每个页面所需的一致内容(标题,页眉,页脚等)都放置在此文件中。 元素<t:body />用于标识页面特定内容的放置位置。 呈现页面时,<t:body />会替换为页面特定的内容。
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Tapestry 5 Demo Application</title>
</head>
<body>
<div class="header">
<h1>Tapestry 5 Demo</h1>
<h2>Springfield</h2>
</div>
...
<t:body/>
<div id="footer">
Design by <a href="http://www.minimalistic-design.net">Minimalistic Design</a>
</div>
</body>
</html>
可以从“开始”页面的模板中删除负责布局HTML元素。 为了包括常见HTML,我们使用组件Layout 。 我们可以简单地在Tapestry名称空间中使用名称与组件类型匹配的元素。 以这种方式引用布局组件,可以将“开始”的模板简化为以下内容:
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
${hello}
</t:layout>
注意<t:layout>组件是如何包装$ {hello}扩展的:这些扩展是特定于页面的内容,但是其他页面将具有表单和表格以及Web应用程序的所有其他陷阱。 另外,您可能想知道“是布局还是布局”? 答案是:是的。 或实际上,答案是Tapestry几乎在所有地方都不关心大小写。
Tapestry组件(如页面)由一个组件模板和一个Java类组成,是时候创建该类了。 您可能已经注意到,模板的外观并没有指示样式表,即使大多数外观是从样式表(和一些相关的图像)派生的。 这是因为在Java类中提供样式表比在模板中提供样式表更容易! Tapestry为此有一个特殊的注释:
@IncludeStylesheet("context:style.css")
public class Layout {
}
“ context:”前缀提示Tapestry在哪里寻找文件。 另一个选项是“ classpath:”,Tapestry组件使用它来引用存储在JAR中的文件。Tapestry负责为客户端建立一个可以引用此类文件的URL。
在这一点上,我们准备抛弃诸如“ Hello World”消息之类的作品,并着手创建一个更现实的应用程序。 我们将从定义模型,要编辑和显示的数据对象开始。
定义模型
现在让我们为示例应用程序创建一个简单的域模型。 我们将创建一个CRUD(创建,检索,更新和删除)之类的应用程序来管理用户,每个用户将如下所示:
为了获得此结果,我们将创建以下模型类:
package t5demo.model;
public class User {
private long id;
private String name;
private String email;
private Date birthday;
private Role role = Role.GUEST;
...
}
添加了属性ID以识别用户; 它将用作外部存储(例如数据库)中的主键。 Enumeration枚举类型描述用户角色:
package t5demo.model;
public enum Role {
ADMIN, USER, GUEST
}
模型类通常保存在一种持久性存储中,例如数据库。 通常使用一组数据库访问对象(DAO)从数据库中获取数据。 在下一部分中,我们将创建一个DAO,并查看如何将其集成到Tapestry页面中。
挂毯IoC
Tapestry IoC(控制反转)是Tapestry的子框架,它充当对象(称为服务)的容器。 Tapestry本身包含120多个相互连接的服务,该框架旨在轻松容纳特定于您的应用程序的服务。 您可以将“控制反转”容器视为Enterprise JavaBeans的一种简化版本,其重点是管理服务对象的生命周期并将它们连接在一起。 连接部分称为依赖注入(DI)。
Tapestry IoC框架的灵感来自于Spring , Guice和Apache HiveMind等知名容器的最佳功能,并结合了简单性,灵活性,易于部署,快速启动和强大的功能。
给定现有容器的数量(这只是部分列表),您可能会问:为什么不使用Spring或Guice? 为什么要发明新东西? Tapestry具有与生命周期和可扩展性相关的要求,而任何现有容器都无法满足这些要求,因此,Tapestry有自己的需求,特别适合其需求。 您甚至可以将Tapestry IoC与Tapestry分开使用。 我们请读者阅读Tapestry IoC文档以获取更多详细信息。
DAO(数据访问对象)是用于从外部资源(即数据库)访问数据的标准模式。 在示例应用程序中,我们将使用UserDAO服务检索用户实例。 请注意,Tapestry IoC和Guice使用术语服务,而Spring使用术语bean 。
public interface UserDAO {
List<User> findAllUsers();
User find(long id);
void save(User user);
void delete(User user);
User findUserByName(String name);
}
如您所见,我们拥有简单的DAO可能需要的所有基本方法。 DAO的实现确实不是很有趣,与本文无关。 在实现实际应用程序时,您将使用对象关系映射工具(例如Hibernate ),并且很可能将使用本机Tapestry 5 Hibernate集成 。 对于我们的示例,我们将仅使用java.util.ArrayList来模拟数据库。 这样,您无需任何其他配置即可启动演示应用程序。
现在我们需要告诉Tapestry,当请求服务UserDAO时要实例化哪个实现。 为此,我们将在应用程序服务包t5demo.services中编辑AppModule类。 您还记得, t5demo是应用程序的根软件包。 服务子包应用于服务。 AppModule类是定义特定于应用程序的服务以及配置Tapestry内置服务的地方。 在AppModule类内部,我们需要将UserDAO服务的接口绑定到其实现:
public class AppModule {
public static void bind(ServiceBinder binder) {
binder.bind(UserDAO.class, UserDAOImpl.class);
}
}
就是这样, ServiceBinder将在UserDAO接口和UserDAOImpl实现之间创建绑定。 UserDAOImpl类具有默认构造函数,可以使用Java Reflection API实例化。 Tapestry 5将创建UserDAOImpl的新实例,并在任何页面或服务需要UserDAO时立即将其注入。 如您所见,不需要XML配置。 您当然可以想象使用UserDAO为类创建单元测试会多么容易。 默认情况下,所有服务都是单例 。 因此,将仅创建UserDAOImpl的一个实例,并且该单个实例将在多个线程之间共享,并且作为扩展,它代表多个应用程序用户工作。
对于我们的演示应用程序,我们想用一些假数据预填充假数据库。 因此,我们将向UserDAOImpl添加一个初始化方法,该方法将在构造函数中调用。
public class UserDAOImpl implements UserDAO {
public UserDAOImpl() { createDemoData(); }
public void createDemoData() {
save(new User("Homer Simpson", "homer@springfield.org", new Date()));
save(new User("Marge Simpson", "marge@springfield.org", new Date()));
save(new User("Bart Simpson", "bart@springfield.org", new Date()));
...
}
....
}
现在是时候显示用户了。
网格组件
在我们的第一页中,我们将在表中显示用户列表:
为了实现此新版本的“开始”页面,我们将结合四个元素:
- 布局组件的总体外观
- Tapestry Grid组件呈现元素表
- UserDAO服务访问用户列表
- 初始页面本身可以集成其他所有内容
在上一节中,我们描述了Tapestry 5 IoC的基础知识。 在本节中,我们将使用AppModule创建的服务。 要注入UserDAO服务,我们只需要声明一个类型为t5demo.services.UserDAO的私有成员,并使用@Inject批注对其进行标记。 Tapestry将根据其类型查找UserDAO服务,并通过该字段将其链接到页面。
public class Start{
@Inject
private UserDAO userDAO;
public List<User> getUsers() {
return userDAO.findAllUsers();
}
}
该页面的模板非常简单,仅由Grid组件的单个实例组成。 Grid的 source参数是必需的,用于获取要显示的数据。 在我们的例子中,源是由getUsers()方法返回的List <User> 。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<t:grid source="users"/>
</t:layout>
如您所见,每一行都代表一个User 。 行的值取自User实例的属性,并根据其类型自动设置格式。 例如,“生日”列已经以区域设置敏感的方式格式化为日期。 在后台,Tapestry正在做一些工作,例如格式化Role枚举以供用户呈现的显示形式(“ Guest”而不是“ GUEST”)。 此外,该表是可排序的。 如果数据源中的可用行数超过某个值,则网格将自动添加一个寻呼机以浏览整个数据。 每个页面上显示的数据行数可以通过Grid的 rowsPerPage参数进行更改。
默认情况下,Grid的列顺序与User类中的getter顺序完全相同。 可以使用Grid的 reorder参数来覆盖此顺序:属性名称的逗号分隔列表。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<t:grid source="users" reorder="role,email,name,id,birthday" />
</t:layout>
因此,我们第一步是定制输出:
诸如id之类的属性实际上是内部属性,对最终用户没有任何意义,应将其省略。 我们可以利用Grid组件的exclude参数,指定要省略的属性列表。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<t:grid source="users" exclude="id,email" />
</t:layout>
应用了exclude参数后,我们的“开始”页面开始成形:
在本文的后面,我们将演示另一个非常强大的Tapestry组件,该组件将直接基于JavaBeans生成用户输入表单。 为了提供我们域模型的一致视图,我们应该使用另一种方法从用户界面中删除属性。 我们将使用@NonVisual注释标识不应属于用户界面的属性 。 该元信息不仅可以被Grid识别,还可以被其他几个Tapestry组件识别(请参阅BeanEditForm组件部分)。 在我们的示例中,我们仅删除用户的标识符。
public class User {
...
@NonVisual
public Long getId() { return id; }
}
并仅使用一个简单的Grid组件声明:
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<t:grid source="users"/>
</t:layout>
结果看起来像这样:
对于我们的下一个自定义,我们将添加编辑用户的功能。 为此,我们将创建一个页面,稍后我们将对其进行讨论。 为了创建编辑链接,我们将覆盖 Grid组件如何渲染一列。 我们将提供一个指向另一个负责编辑用户的页面的链接,而不是简单地呈现User的name属性。
这是通过另一个特殊的模板元素<t:parameter>完成的。 实际上,此元素是一种将组件模板的一部分作为参数传递到组件中的方法。
<t:parameter>元素的名称用于确定要覆盖其呈现的属性。 按照约定,名称应由属性名称和后缀Cell组成 (也可以用Header后缀覆盖列标题)。
在我们的示例中,<t:parameter>元素名称为nameCell ,因此Grid组件将使用提供的PageLink组件来呈现包含每个User名称的列。
PageLink组件呈现HTML链接到page参数指定的页面 。 上下文参数中提供了指示页面所需的其他信息。 在此示例中,用于编辑用户的页面将称为“ 编辑” ,并将接受用户ID作为参数。 我们还将需要一个属性来存储要呈现的User ,以便我们可以提取其name和id属性。 我们将需要在Start类中创建一个名为user的属性。 然后,我们可以将Grid组件的row参数绑定到此属性。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<t:grid source="users" row="user" >
<t:parameter name="nameCell">
<t:pagelink page="edit" context="user.id">${user.name}<
/t:pagelink>
</t:parameter>
</t:grid>
</t:layout>
您可以在此处看到扩展和其他属性表达式不限于简单的属性名称。 您可以使用点分符号来导航一个或多个属性,以读取(以及稍后将要编辑的)嵌套属性。
我们可以通过这种方式自定义任意数量的Grid列。
不要忘记将属性用户添加到Start类:
public class Start {
...
private User user;
public User getUser() { return user; }
public void setUser(User user) { this.user = user; }
}
Tapestry会抱怨“编辑”页面上至少没有一个班级:
public class Edit {
}
我们稍后可以填写“编辑”页面的详细信息。
现在我们几乎完成了“开始”页面:
如果我们想添加另一个与User属性不对应的列怎么办? 例如,我们可能需要一个列,以便我们可以删除不需要的用户。 这可以使用虚拟属性来完成,但是需要花费更多的精力。
Grid组件通过首先为对象生成BeanModel来决定如何显示Bean(例如User对象)。 通过自己创建然后自定义BeanModel ,我们可以完全控制Grid的运行方式。 我们必须将新对象注入“开始”页面:
- org.apache.tapestry.services.BeanModelSource-负责为特定bean类创建BeanModel的服务。
- org.apache.tapestry.ComponentResources-提供BeanModelSource所需的一些框架功能。
public class Start {
...
@Inject
private BeanModelSource beanModelSource;
@Inject
private ComponentResources resources;
public BeanModel getModel() {
BeanModel model = beanModelSource.create(User.class, false, resources);
model.add("delete", null);
return model;
}
}
在“开始”页面的模板中,我们进行了两项进一步的更改:首先,向Grid组件明确告知其应使用的模型,其次,提供一个<t:parameter>块以呈现delete列。
我们将使用ActionLink组件来找出用户何时要删除用户。 ActionLink将在“开始”页面上触发事件。 我们可以通过提供一个特殊命名的方法来观察事件。 提供方法名称onActionFromDelete()通知Tapestry当在其ID为“删除”的组件中触发“动作”事件时,应调用该方法。
但是要删除哪个用户 ? 再次,我们将id作为上下文传递; 然后,该值将用作事件处理程序方法的方法参数:
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<t:grid source="users" row="user" model="model" >
<t:parameter name="nameCell">...</t:parameter>
<t:parameter name="deleteCell">
<t:actionlink t:id="delete" context="user.id">Delete<
/t:actionlink>
</t:parameter>
</t:grid>
</t:layout>
在Start类中,我们提供事件处理程序方法:
public class Start {
...
void onActionFromDelete(long id) {
user = userDAO.find(id);
if (user != null)
userDAO.delete(user);
}
}
事件处理程序方法不必公开。 首选的可见性是包私有的(不带修饰符的可见性)。 Tapestry和同一包中的其他类(例如单元测试)可以调用这种方法,但它不是页面公共API的一部分。
现在,我们已经完成了“开始”页面,可以继续进入“编辑”页面:
导航模式
您还记得,我们需要实现一个页面,可以在其中编辑User 。 让我们将页面命名为Edit 。 在本节中,我们描述如何从“ 开始”页面导航到“ 编辑”页面。 为此,让我们在Web应用程序的根文件夹中创建一个模板Edit.tml (我们已经创建了一个空类t5demo.pages.Edit )。 在本节中,我们将在Tapestry 5中描述导航逻辑。下一部分将介绍编辑逻辑。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h1>Edit/Create Citizen</h1>
</t:layout>
由于我们的页面将编辑用户,因此我们需要访问UserDAO 。 要注入它,我们创建一个私有变量,并像以前一样使用@Inject批注对其进行标记:
public class Edit {
@Inject
private UserDAO userDAO;
...
}
我们的编辑页面需要知道要编辑的用户的标识符。 在开发Web应用程序时,这种导航模式(通常称为主/细节)非常普遍。 Tapestry 5使用RESTfull 页面激活上下文机制对其提供了明确的支持。
REST代表代表性状态转移 。 它是一种分布式超媒体系统的软件体系结构。 REST的中心原则是由URI标识的资源,以修改资源,客户端通过标准接口(例如HTTP)进行通信并交换这些资源的表示形式。 有关REST优点的更多信息,您可以在Wikipedia上阅读,在我们的案例中,重要的是要看到REST鼓励我们将应用程序状态作为URL的一部分,而这正是页面激活上下文将为我们提供帮助的地方。
Tapestry中的所有页面都可以支持页面激活上下文。 页面激活上下文包含可以在请求中保留的页面状态。 根据REST范例,页面激活上下文将附加到页面的URL。 页面激活上下文与在HTTP会话中存储页面状态非常相似。 但是,在这种情况下,您将需要一个活动会话,并且页面状态不可收藏。
在Tapestry中,页面积极参与提供页面激活上下文,并且同样参与处理该激活上下文。 这是通过为两个事件提供事件处理程序方法来处理的:“钝化”和“激活”。 对于“钝化”事件,页面应提供一个代表其完整内部状态的值(或值数组)。 对于“ 编辑”页面,即正在编辑的用户的ID。 当对该页面的后续请求(例如表单提交)得到处理时,将通过“ activate”方法提供页面激活状态,并且页面将从提供的值中恢复其完整状态。
因此,我们的第一步是添加以下两个事件处理程序方法:
public class Edit {
...
private User user = null;
private long userId = 0;
void onActivate(long id) {
user = userDAO.find(id);
userId = id;
}
long onPassivate() {
return userId;
}
}
在onActivate()方法内部,我们期望单个长值作为页面激活上下文,在onPassivate()方法内部,我们提供该确切值。 激活后,“编辑”页面使用注入的UserDAO服务从数据库中获取用户并将其存储在私有变量中。
现在,让我们看一下呈现上下文的页面的URL。 如果导航到演示应用程序的第一页,您将看到用于编辑用户的 URL具有以下格式: http:// localhost:8080 / app / edit / 3 。 考虑到Tapestry中的所有页面名称都不区分大小写 ,您可以看到此URL将呈现“ 编辑”页面,并且要编辑的用户具有标识符3 。 该网址简单明了; 它是RESTfull,因此是可收藏的(我们可以将该URL存储在浏览器的书签中,明天再返回到相同的页面,如果任何状态都存储在HttpSession中 ,则不可能)。
现在我们已经能够跟踪要编辑的用户 ,接下来的任务是提供一个用户界面。 这样做非常简单。
BeanEditForm组件
在上一章中,我们开始在可以创建和/或修改现有用户的页面上工作。 我们创建了一个页面类,该页面类能够维护其状态并了解要编辑的用户。 与大多数面向组件的Web框架一样,我们需要创建一个表单组件和嵌套字段。 模型对象的每个值都应绑定到适当类型的字段(短文本的文本字段,布尔值的复选框,枚举类型的下拉列表等)。 我们还想考虑输入验证。 这是通常的方法,但是Tapestry有一个更简单的方法。
首先,我们需要为用户私有变量创建一个公共获取方法。 您还记得,一旦页面从页面激活上下文恢复了其状态,便会初始化此变量:
public class Edit {
...
public User getUser() {
return user;
}
...
}
到目前为止,这并不难。 是吗 现在,事件变得更简单了,我们需要在模板中添加以下行:
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h1>Edit/Create Citizen</h1>
<div id="section">
<t:beaneditform t:id="form" t:object="user"/>
</div>
</t:layout>
我们正在使用称为BeanEditForm的 Tapestry组件。 它只有一个必需的参数对象 ,该对象应与我们要编辑的对象绑定到属性。 就这样。 现在,您可以刷新“ 编辑”页面,并看到其中充满了自动生成的表单,其中包含已编辑模型类的每个属性的字段。
Tapestry 5再次做了很多工作来简化开发人员的生活。 马上,我们就有了一个可用的用户界面,并且与Grid组件一样,还有大量的自定义空间。 该表格反映的细节很少。 字段标签是从驼峰大小写正确转换的(“ userName”将显示为“ User Name”),枚举值将显示为具有正确选项集的下拉列表(也已转换为便于阅读),并且使用了弹出日历编辑日期属性。 BeanEditForm还遵守@NonVisual批注,因此省略了id字段。 因为Grid和BeanEditForm组件都在同一个BeanModel上运行,所以它们将继续保持一致。
BeanEditForm组件非常灵活,并提供了许多自定义可能性:
- 您可以通过简单地对源代码中的getter进行重新排序或以编程方式对生成的表单中的字段进行重新排序。
- 您可以使用@NonVisual批注或组件参数中提供的以逗号分隔的属性列表来隐藏字段。
- 您可以用自定义块完全替换生成的表单的某些部分,以不同的方式渲染某些输入字段(例如密码字段)。
- 您可以动态修改已编辑bean的元信息,就像之前对Grid所做的一样。
- 您可以更改表格上的标签,当然,您可以获得完整的国际化支持。
- 您添加/删除表单内的按钮。
我们的编辑表单示例的第二步是验证。 由于我们使用的是表单的自动生成,因此验证约束的最佳位置是我们的模型对象。 要添加验证约束,只需使用@Validate批注批注特定字段的访问方法。
public class User {
...
@Validate("required")
public String getName() {
return name;
}
@Validate("required,regexp")
public String getEmail() {
return email;
}
...
}
在我们的示例中,我们只需要填写名称和电子邮件属性。 @Validate批注接受一个字符串参数,它将描述验证约束。 Tapestry 4用户可能熟悉此参数的语法,实际上,您只在逗号分隔的列表内列出了约束。 可能的约束名称是必需的 , max , min , maxLength , minLength和regexp 。 正则表达式验证器需要两个参数:验证失败时要显示的表达式和人类可读消息。 这两个验证参数都很长,并且由于所有特殊字符而不适用于模板。 幸运的是,Tapestry页面和应用程序可以具有消息目录,这是存储长而复杂的字符串的自然位置。 为此,我们在软件包t5demo.pages中创建了一个Edit.properties文件。 如您所见,表达式的键是email-regexp 。 该密钥由要验证的属性名称和验证者名称组成。 消息的密钥包含另一个后缀-message 。
email-regexp=^[a-zA-Z0-9._-]+@[a-zA-Z0-9-]+\.[a-zA-Z.]{2,5}$
email-regexp-message=Provided email is not valid
默认情况下,Tapestry中的所有表单均启用了客户端验证。 如果您尝试提交带有必填字段的表单,则会立即在气泡中看到一条消息,如下图所示:
显然,客户端验证与服务器端重复。 禁用javascript时,您将在表单提交后看到错误消息。 下图中的错误消息是在服务器端生成的:
一旦用户在客户端提交了表单,该操作就会在服务器端进行。 假设没有验证错误, BeanEditForm组件将触发“成功”(例如,成功提交表单)事件。 我们再次提供一个事件处理程序方法onSuccess() (它将与触发“成功”事件的任何组件匹配,但BeanEditForm是唯一的候选者,因此不必特别具体)。 这次,我们不仅要执行操作(将User更新回数据库),而且还要导航回到Start页面。
public class Edit {
...
public Object onSuccess() {
userDAO.save(user);
return Start.class;
}
...
}
Tapestry以一种特殊的方式解释事件处理程序方法上的返回值:作为将页面呈现给用户的指令。 返回页面实例或页面类,甚至页面名称(以String形式)都导航到该适当的页面。 还支持其他返回值(有关完整列表,请参见Tapestry文档)。
Tapestry内置的验证是有限的:它们可以在客户端和服务器上运行,但是范围有限。 它们是语法,仅基于提供的字符。 假设我们要执行一些与业务逻辑有关的验证,例如,以确保在编辑用户名时,新名称是唯一的。
同样,Tapestry通过触发事件来使用我们的代码。 该事件被命名为“ validateForm”,就像在“成功”事件之前触发的一样。
public class Edit {
...
@Component
private BeanEditForm form;
public void onValidateForm() {
User anotherUser = userDAO.findUserByName(user.getName());
if (anotherUser != null && anotherUser.getId() != user.getId()) {
form.recordError("User with the name '" + user.getName() + "' already exists");
}
}
}
如您所见,我们可以使用BeanEditForm.recordError()以编程方式向表单添加错误。
现在,在对现有用户进行编辑之后,我们只需要考虑创建新用户。 BeanEditForm组件的行为非常好:当edited属性为null时 ,它将仅创建bounded类的新实例并将其设置回页面。 使用此行为,我们只需要为用户变量添加一个公共设置器。 激活页面后,传入的标识符为0时我们需要注意。
public void onActivate(long id) {
if (id > 0) {
user = userDAO.find(id);
this.userId = id;
}
}
就是这样 现在,当我们将用户标识符传递到“ 编辑”页面时,它将编辑用户。 如果未传递标识符或传递的标识符等于零,则“ 编辑”页面将仅创建User的新实例。
挂毯5中的Ajax
Ajax代表异步JavaScript和XML。 Ajax是一种特殊技术,用于创建动态且更用户友好的网站和Web应用程序。 使用Ajax,可以通过在客户端和服务器之间异步交换数据来实现网页的响应性和交互性。 Ajax是一项非常强大的技术,您可以在Wikipedia上阅读有关它的更多信息
Tapestry Ajax支持基于流行的Prototype和script.aculo.us JavaScript库。 由于这两个库捆绑在Tapestry内,因此不需要单独的部署或配置。
通常,在启用Ajax的应用程序中,页面的某些部分会根据用户请求进行刷新。 在Tapestry中, ActionLink组件可用于触发Ajax操作。 作为示例,我们将通过添加另一个包含查看用户详细信息的链接的列来再次自定义Grid组件。 让我们通过添加名为view的列来更新Grid的模型,该列将呈现为ActionLink 。 首先,我们将新的人工属性添加到BeanModel中 :
public class Start{
...
public BeanModel getModel() {
BeanModel model = beanModelSource.create(User.class, false, resources);
model.add("delete", null);
model.add("view", null);
return model;
}
}
然后,我们通过添加另一个<t:parameter />来定义属性视图的呈现。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h1>Springfield citizens</h1>
<t:grid source="users" model="model" row="user">
<t:parameter name="nameCell">...</t:parameter>
<t:parameter name="deleteCell">...</t:parameter>
<t:parameter name="viewCell">
<t:actionlink t:id="view" zone="viewZone" context="user.id">View</t:actionlink>
</t:parameter>
</t:grid>
</t:layout>
如前所述, ActionLink是在服务器端触发动作的组件。 默认情况下,触发的操作导致页面的完全刷新。 参数区域告诉ActionLink进行Ajax调用并更新区域,该区域的客户端ID是参数的值。 区域是Tapestry的方法,用于对客户端执行部分更新。 区域组件标记了页面的一部分,可以动态更新。 首先让我们使用客户端ID viewZone创建一个Zone。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h1>Springfield citizens</h1>
<t:zone t:id="viewZone"/>
<t:grid source="users" model="model" row="user">...</t:grid>
</t:layout>
<t:block>可以定义用于更新区域的内容。 <t:block>是一种自由浮动的<t:parameter> ,可以注入到组件中。 默认情况下,不渲染块内的内容。 我们创建一个ID为userDetails的块,并显示存储在该块内部页面属性detailUser中的用户的某些属性。
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h1>Springfield citizens</h1>
<t:zone t:id="viewZone"/>
<t:block id="userDetails">
<h2>${detailUser.name}</h2>
<ul style="padding-left: 40px;">
<li>Identifier: ${detailUser.id}</li>
<li>Email: ${detailUser.email}</li>
<li>Birthday: ${detailUser.birthday}</li>
</ul>
</t:block>
<t:grid source="users" model="model" row="user">...</t:grid>
</t:layout>
单击ActionLink 视图时 ,将触发一个动作事件。 因此,将调用onActionFrom View的匹配方法。 方法参数表示用户标识符,用于在数据库中查找用户。 检索到的用户存储在私有属性detailUser中 ,我们在<t:block>内部使用它。
如上所述,动作方法的返回类型用于确定响应的类型。 在传统的非Ajax请求中,action方法的返回类型用于确定呈现响应的页面。 为了执行部分响应,我们可能会返回其标记将用于更新我们的Zone内容而不刷新整个页面的组件或块。 为此,我们通过@Inject注释注入最近创建的块。 请注意,要使注入正常工作,属性名称属性类应与组件客户端ID和组件类相同(否则,您可以自定义@Inject注解 )。 注入的块由我们的操作方法onActionFromView返回。 渲染该块,并将其标记插入到区域中。
public class Start{
...
@Inject
private Block userDetails;
private User detailUser;
public User getDetailUser() { return detailUser; }
Block onActionFromView(long id){
detailUser = userDAO.find(id);
return userDetails;
}
}
在这里,我们介绍了基本的Ajax功能-部分页面刷新。 使用Tapestry 5,这种功能非常容易实现,您不需要自己编写一行JavaScript。 嵌入式JavaScript库提供的功能已可以跨浏览器兼容生产。
创建自己的ajax组件
显然,仅使用部分页面刷新就无法实现Web应用程序中的完全交互,现在我们想看看如何创建自己的Ajax组件。 在本部分中,我们将创建一个Tapestry 5组件,该组件将使用script.aculo.us JavaScript库中的Ajax.InPlaceEditor 。 InPlaceEditor允许您编辑页面上的一些文本。 单击该文本将即时创建一个输入字段,其值就是我们单击的文本。 提交表单后,我们用字段的值替换页面上的原始文本。
首先,我们使用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")
public class InPlaceEditor{
private static final String PARAM_NAME = "t:InPlaceEditor";
@Environmental
private PageRenderSupport pageRenderSupport;
@Inject
private ComponentResources resources;
@Inject
private Request request;
}
接下来,我们创建一个名为value的必需组件参数。 它绑定到InPlaceEditor容器的属性,并将被编辑。
public class InPlaceEditor{
...
@Parameter(required = true)
private String value;
...
}
现在让我们编写一个渲染组件的方法。 Tapestry组件的渲染分为几个阶段。 到目前为止,我们没有提到这些阶段。 由于我们希望使本文保持简单,因此我们将不在本文中讨论它们。 如果您想了解有关它们的更多信息,请参考Tapestry文档。
为了渲染我们的组件,我们插入了AfterRender阶段。 此阶段通常与BeginRender结合使用,以呈现html标签并装饰组件的模板。 由于我们的组件既没有标签也没有模板,因此AfterRender将做到这一点。 为了插入AfterRender阶段,我们实现了afterRender(MarkupWriter)方法。 首先,我们尝试获取标记中使用的元素名称。 如果未定义元素名称,则取<span />。 然后,我们为组件分配一个唯一的ID。 在下一步中,我们打开html元素并呈现非正式参数。 非正式参数是不是由我们的组件定义的参数,而是由用户在模板内部提供的。唯一的形式参数(为我们的组件指定)是value 。 典型的非正式参数的一个示例是style =“ someCssStyle” ,我们通常对解析用户提供的样式不感兴趣,但是在渲染组件时我们希望保留它。 写入值后,我们关闭最近打开的元素。 最后,我们编写JavaScript代码以创建Ajax.InPlaceEditor 。
JavaScript“类” Ajax.InPlaceEditor的构造函数采用三个参数:
- 第一个是支持就地编辑的元素的客户端ID。
- 第二个是将更改后的值提交到的URL。
- 第三个是包含选项的JSON对象。
在上一节中,我们学习了用于触发服务器端操作的ActionLink组件。 在此示例中,我们使用服务ComponentResources以编程方式创建ActionLink。 单击链接后,将触发事件编辑 。 此事件名称将用于匹配触发事件时要调用的相应方法。 传递给InPlaceEditor构造函数的JSONObject仅包含一个键/值对,该键/值对用于命名包含Ajax.InPlaceEditor提交的值的请求参数。 请注意, PageRenderSupport用于将JavaScript代码添加到页面中。 该服务将在页面中添加一个回调JavaScript函数,该函数将在加载页面DOM时调用。 您稍后将看到的结果HTML现在是完整的方法代码:
void afterRender(MarkupWriter writer){
String elementName=resources.getElementName();
if(elementName==null)elementName="span";
String clientId = pageRenderSupport.allocateClientId(resources.getId());
writer.element(elementName, "id", clientId);
resources.renderInformalParameters(writer);
if (value != null)
writer.write(value);
writer.end();
JSONObject config = new JSONObject();
config.put("paramName", PARAM_NAME);
Link link = resources.createActionLink("edit", false);
pageRenderSupport.addScript("new Ajax.InPlaceEditor('%s', '%s', %s);",
clientId, link.toAbsoluteURI(), config);
}
最后,我们提供操作方法以匹配触发的事件。 单击ActionLink后,将触发事件编辑 。 因此,我们的操作方法应在Edit上命名。 在此方法中,我们从指定的请求参数获取提交的值,并将其分配给bounded属性。 为了用新值更新客户端,我们返回一个TextStreamResponse ,它提供了要发送到客户端的数据流。 可能会返回其他类型来更新客户端。
- 组件或块。 在上一节中,我们使用了一个块来更新区域。
- JSONObject
Object onEdit(){
value = request.getParameter(PARAM_NAME);
return new TextStreamResponse("text", value);
}
让我们使用InPlaceEditor创建一个示例页面。 让edit为要编辑页面的属性。 通过@Persist批注标记字段,我们告诉Tapestry将字段的值从一个请求保留到另一个请求。 此外,我们创建了公共的getter / setter方法。
public class InPlaceEditorExample {
@Persist
private String edit="Please click here";
public String getEdit() { return edit; }
public void setEdit(String edit) { this.edit = edit; }
}
在模板中,我们将value参数绑定到属性edit :
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h1>Using InPlaceEditor</h1>
<div t:type="InPlaceEditor" value="edit"/>
</t:layout>
这将导致以下html代码:
...
<div id="inplaceeditor">Please click here</div>
...
<script type="text/javascript">
Tapestry.onDOMLoaded(function() {
new Ajax.InPlaceEditor('inplaceeditor',
'/app/inplaceeditorexample.inplaceeditor:edit',
{"paramName":"t:InPlaceEditor"});
});
</script>
当您调用页面http:// localhost:8080 / app / inplaceeditorexample时 ,您将看到属性编辑的默认值,即, 请单击此处 。
在单击编辑器后,您将看到以下内容:
只需键入一些文本,然后单击确定按钮即可。 属性编辑将通过提交的值进行更新。 当然,我们应该为该组件添加更多功能。 例如,我们可以添加编辑器字段的验证。 但是,本文的目的是向您提供Tapestry功能的概述,而不是开发可以在生产中使用的全套组件。
结论
我们展示了从头开始创建Tapestry应用程序非常简单,以及使用Tapestry组件时您可以快速而高效地工作。 我们已经使用Maven的Java NCSS插件为我们的应用程序计算了许多非注释代码行,结果如下:
- 我们的演示应用程序的Java代码由205个NCSS(无注释源代码行)组成。
- 我们的页面模板为56行,其中Layout.tml中有34行存储页面设计。
- 两个最长的Java类是UserDAOImpl和User ,它们在一起是68行。
- 最大的页面类是t5demo.pages 。仅以27行Java代码和12行模板代码开始。 使用Tapestry 5,我们设法提供了具有删除和查看功能的可排序和可分页的表。 我们能够使用Ajax进行部分页面渲染,而仅使用39行代码即可。
显然,代码行数并不是我们应该使用的唯一度量。 但是很显然,您拥有的代码行越多,应用程序的维护,理解和测试就越有挑战性。
Tapestry团队付出了巨大的努力来改善框架的开发人员友好度:
- Tapestry 5应用程序几乎需要零配置。 对于大多数零件,通常需要进行配置,已经有一些合理的默认值。 如您之前所见,在示例中我们没有创建任何XML行。
- 独特的类重新加载功能可提高开发效率。 每次在页面或组件的代码中发现一个小错误时,您就不再需要重新启动应用程序容器。 页面和组件类会自动重新加载。
- 开发人员友好的错误报告不仅显示行号和代码段,而且还提出了可能的解决方案。
Tapestry 5从许多应用程序领域收集了最佳实践,例如受Ruby on Rails启发的配置约定和脚手架惯例,无XML依赖注入和Google Guice启发的配置,可扩展性的REST-full URL。 Tapestry产生的输出是有效的(X)HTML。 诸如Grid和BeanEditForm之类的复杂组件是可访问的,无表的和CSS可定制的。 默认输出编码为UTF-8,框架支持本地化。
在我们的文章中,我们仅展示了Tapestry 5新功能的一小部分。 我们没有将Tapestry IoC深入到Tapestry应用程序,mixin和资产的本地化。 我们还没有提到Hibernate , Spring, Seam , Acegi等许多Tapestry 5集成模块。 但是,我们希望可以向您展示Tapestry 5的开发过程有多简单,以及构建生产就绪型和强大应用程序的速度。
致谢
我们要感谢Tapestry的创建者Howard M. Lewis Ship对本文的审阅和校对。 我们感谢他提供的宝贵宝贵意见。 此外,我们还要感谢他创建了Tapestry。 最后但并非最不重要的一点是,我们要感谢整个Tapestry团队以及围绕Tapestry管理开源项目的所有人所做的出色工作。
快速入门和演示应用程序
可以从Google Code上的tapestry4nonbelievers项目中下载我们为本文开发的Tapestry Quickstart和Demo应用程序的源代码。 这两个应用程序都打包为WAR文件,并且可以导入到您选择的IDE中。
参考资料
附录
开始构建Tapestry应用程序的最简单方法是使用Maven和Tapestry Quickstart原型。 Maven原型是预定义的项目模板,可用于快速启动新项目。 Tapestry快速入门原型定义了项目目录结构,初始元信息甚至一些基本的应用程序类。 如果您熟悉Apache Struts,那么您可能会知道Struts发行版随附的struts-blank.war文件。 该文件包含一个完全可部署且正常运行的空Struts应用程序。
Tapestry 5 Maven原型提供了一个空的Tapestry应用程序,该应用程序由单个页面组成,可以被编译并部署到servlet容器中。 如果您已安装Maven,则只需执行以下命令:
mvn archetype:create -DarchetypeGroupId=org.apache.tapestry -DarchetypeArtifactId=quickstart \
-DgroupId=t5demo -DartifactId= app -DpackageName= t5demo -Dversion=1.0.0-SNAPSHOT
Maven使用原型创建Tapestry应用程序名称app 。 Maven创建一个子目录app以存储新项目。 让我们检查一下Maven为您生成了什么。 如果您熟悉Maven,则可以识别默认的Maven目录结构。
要使用Jetty servlet容器启动应用程序,请切换到新创建的应用程序目录并执行以下命令:
mvn jetty:run
Maven将编译生成的应用程序,下载(只是第一次!)Jetty servlet容器,然后启动编译的应用程序。 启动该应用程序后,您可以在浏览器中以http:// localhost:8080 / app对其进行访问 。