咱们开始来构建一个基础的Hi-Lo猜谜游戏。html
在这个游戏中,计算机会选择一个介于1到10之间的数字。你尝试猜出这个数字,点击一些连接。最后,计算器会告诉你确认目标数字你须要猜多少次。即便是像这样一个简单的示例,也能体现Tapestry中的几个重要概念:java
l 将一个应用程序分段放到各自独立的几个page中web
l 将信息从给一个page传送到另一个page数据库
l 响应用户的交互apache
l 在服务器端session中存储客户端信息编程
咱们将用几个小块的来构建这个小巧的应用程序,使用Tapestry来进行这种迭代式的开发很是容易。api
页面流程很是简单,包含三个page:Index(起始page),Guess以及GameOver。Index page对应用程序进行介绍,并包含一个开始猜谜游戏的连接。Guess page像用户显示10个连接,加上一些诸如“too low”,“too high”的提示信息。GameOver page告诉用户在找到目标数字以前他们已经猜想了多少次。浏览器
先来处理Index page和模板。像下面这样建立Index.tml:安全
<html t:type="layout" title="Hi/Lo Guess"服务器
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
<p>
I'm thinking of a number between one and ten ...
</p>
<p>
<a href="#">start guessing</a>
</p>
</html>
而后编辑对应的Java类,Index.java,删除其正文(不过目前你能够把import语句仍保留在那里)。
Index.java
package com.example.tutorial1.pages;
public class Index
{
}
运行应用程序,咱们将会看到起始界面:
然而,如今点击这个连接一点反应都不会有,由于它如今还只是一个预留用来占位的<a>标记而已,并非一个实际的Tapestry component。让咱们来想一想当用户点击这个连接时应该要发生些什么:
l 会有一个介于1到10之间的随机数据被选出来
l 花费的猜想次数应该被重置为0
l 用户应该被指引至Guess page以进行猜想
第一步咱们得找到用户应该在何时点击这个“start guessing”连接。在一个典型的web应用程序框架中,咱们最开始考虑的多是URL和处理器,或者是某些类型的XML配置文件。不过如今是Tapestry了,所以与咱们相伴工做的是类中的component和方法。
首先是component。咱们想要在继续Guess page以前执行一个动做(选择数字)。ActionLink component就是咱们所须要的;它会常见一个带有URL的连接,这个URL会触发咱们代码中的一个动做事件……不过到这里咱们已经超前了。首先仍是要把<a>标记转成一个ActionLink component:
Index.tml(局部)
<p>
<t:actionlink t:id="start">start guessing</t:actionlink>
</p>
若是你刷新浏览器并将鼠标停留在“start guessing”上面,将会看到起URL如今已是 /tutorial1/index.start 了,其表示的是page的名称(“index”)和component的id(“start”)。
如今若是你点击连接,页面会显示一个错误:
Tapestry要告诉咱们的是须要为这个事件提供某种类型的事件处理器。这是个什么东西呢?
事件处理器就是Java类中的一个带有特殊名称的方法。方法的形式如 onEventnameFromComponent-id……这里咱们想要的是一个叫作onActionFromStart()的方法。那咱们是如何知道“action”才是正确的事件名称的呢?由于ActionLink就是这么规定的,这也是为何它被命名为ActionLink的缘由。
Tapestry再一次为咱们提供了选择的余地;若是你不喜欢约定的命名方式,能够把@OnEvent注解放在方法的前头,它能给予你按本身喜爱命名方法的自由。有关于此的详细信息,见Tapestry的用户指南。本教程咱们仍是坚持使用约定命名的方式吧。
在处理一个component事件请求(由ActionLink component的URL触发的请求类型)时,Tapestry将会找到这个component并在其上触发一个component 时间。这是咱们服务器端的代码所须要的回调,用来了解用户正在客户端上面作些什么。让咱们先从一个空的事件处理器开始:
Index.java
package com.example.tutorial1.pages;
public class Index
{
void onActionFromStart()
{
}
}
在浏览器中咱们能够经过点击刷新按钮的操做来从新尝试一下刚刚失败的component 事件请求……或者咱们也能够重启应用程序。两种状况下,咱们看到的时候默认的行为效果,就是简单地从新渲染了一下page。
注意事件处理方法并没必要得是public的;它也能够是protected、private或者package private(如这个示例)的。根据约定,这样的方法都应该是package private的,如没有其它理由那么最少许的字符输入量就是依据了。
呃……目前只能对于咱们会让方法获得调用这一点保持信任。这不怎么好……什么能快速的让咱们确认这件事情呢?一种方法就是让方法抛出异常,不过这有点笨啊。
那么这样如何:向方法添加一个@Log注解:
Index.java(局部)
import org.apache.tapestry5.annotations.Log;
. . .
void onActionFromStart()
{
}
接下来在你点击连接时,就会在Eclipse的console面板中看到以下信息:
[DEBUG] pages.Index [ENTER] onActionFromStart()
[DEBUG] pages.Index [ EXIT] onActionFromStart
[INFO] AppModule.TimingFilter Request time: 3 ms
[INFO] AppModule.TimingFilter Request time: 5 ms
@Log 注解会指示Tapestry对方法的进入和退出记录日志。你就能够看到传入方法的参数,还有方法的返回值了……固然还有方法抛出的异常。这是一个强大的调试工具。这就是Tapestry的元编程能力的一个例子,咱们会在本教程中至关多的用到它。
为何咱们会看到一次点击有两次请求呢?由于Tapestry运用了一种基于Post/Redirect/Get模式的方法,每次的component事件以后Tapestry通常执行的都是一次重定向redirect。所以第一次请求是来处理动做的,第二次请求是来从新渲染Index page的。你能够在浏览器中看到,由于URL仍旧是“/tutorial1”(渲染Index page的URL)。后续咱们会回过头来扯这个。
咱们已经准备好进行下一步了,涉及到将Index和Guess page链接到一块儿。Index会选择一个目标数字给用户去Guess,而后“接力棒”交给Guess page。
开始考虑关于Guess page的事情。它须要一个变量,其中存储的是目标值,还须要一个可让Index page调用的方法,由其来设置目标值。
Guess.java
package com.example.tutorial1.pages;
public class Guess
{
private int target;
void setup(int target)
{
this.target = target;
}
}
在跟Index.java所处的同一个文件夹下面常见Guess.java文件。接下来,咱们能够修改Index来调用Guess page类的setup()方法:
Index.java(修订版)
package com.example.tutorial1.pages;
import java.util.Random;
import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Log;
public class Index
{
private final Random random = new Random(System.nanoTime());
@InjectPage
private Guess guess;
Object onActionFromStart()
{
int target = random.nextInt(10) + 1;
guess.setup(target);
return guess;
}
}
如今新的事件处理方法能够选择目标数字了,而且会告诉Guess page这个事情。由于Tapestry是一个被管理起来的环境,因此咱们不用建立Guess的一个实例……管理Guess page的生命周期是Tapestry该管的事情。所以,咱们该找Tapestry去要Guess page,就使用@InjectPage 注解。
Tapestry的page或者component中全部的属性域都必须是非public的。
当咱们有了这个Guess page实例,就能够跟往常同样调用其方法了。
从事件处理器方法返回一个page实例,会指示Tapestry将一个客户端重定向发送给返回的page,而不是发送一个重定向给当前的page。如此当用户一点击“start guessing”连接,就能够看见Guess page了。
在你建立本身的应用程序时,要确保存储在final变量中的对象是线程安全的。彷佛有违常理,但final是在许多个线程之间共享的。通常的实例变量则不是。幸运的是,Random的实现事实上就是线程安全的。
所以……让咱们点击连接试试会看到什么:
啊!咱们没有建立Guess page 的模板。Tapestry确实但愿咱们建立一个,因此咱们最好这样作。
src/main/resources/com/example/tutorial/pages/Guess.tml
<html t:type="layout" title="Guess The Number"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
<p>
The secret number is: ${target}.
</p>
</html>
点击浏览器上的返回按钮,而后再次点击“start guessing”连接。离咱们的目标更接近了:
若是你向下滚动,会发现有一行说Guess.tml模板有一行存在错误。咱们有一个叫作target的域,但它是private的,并且没有对应的属性,所以Tapestry不能访问到它。
咱们只须要加上确实的JavaBean访问器getTarget()(最好setTarget()也加上)就好了。或者咱们也可让Tapestry来编写这些方法:
@Property
private int target;
@Property注解很是简单的指示Tapestry为你编写getter和setter方法。你仅仅只须要在你准备在模板用引用这个属性域时才这样作。
咱们已经很是接近了,不过还有最后一个大的事情要处理。当你刷新了页面,你会看到target变成了0!
以前提过,Tapestry会在处理完事件请求以后发送给客户端一个重定向。这意味着页面的渲染发生在一个全新的请求之中。同时,每一个请求的最后,Tapestry都会将每一个实例变量的值擦除。所以这就意味着在component 事件请求期间target是一个非0的数字……但有时若是从网页浏览器处进来一个新的page渲染请求要渲染Guess page,那target属性域的值就会回到其默认的0。
这里的解决方案就是标记出其值应该在从一个请求到接下来的请求(再接下来、再接下来……)中持续存在。这就是@Persist注解的由来了:
@Property
@Persist
private int target;
这跟数据库的持久化(这在以后的章节中会讲到)一毛钱的关系都木有。只是意在让值存储在请求之间共享的HttpSession中。
回到Index page并再次点击连接。最后,咱们有了一个目标数字:
对于咱们的起步阶段这就够了。让咱们把Guess page 整出来,让用户能够作猜想。咱们将显示猜想的次数,而且在他们作猜想的时候让次数累加。以后咱们要关注猜想是高了仍是低了,或者已经选择了正确的值。
在构建Tapestry的page时,你有时会先从Java代码开始,并构建对应的模板,而有时又从模板开始,并构建对应的Java代码。两种方式都是能够的。这里,咱们先从模板中的标记开始,而后再来理会Java代码该怎么写才对。
Guess.tml(修订版)
<html t:type="layout" title="Guess The Number"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
xmlns:p="tapestry:parameter">
<p>
The secret number is: ${target}.
</p>
<strong>Guess number ${guessCount}</strong>
<p>Make a guess from the options below:</p>
<ul class="list-inline">
<t:loop source="1..10" value="current">
<li>
<t:actionlink t:id="makeGuess" context="current">${current}
</t:actionlink>
</li>
</t:loop>
</ul>
</html>
如此看起来咱们是须要一个从1开始guessCount属性的。
咱们也看到了一个新的component,Loop component。Loop component会迭代传入其source参数的值,对于每一个值都在其正文中渲染一遍。在渲染其正文以前,它会更新其value参数。
这个特殊的属性表达式 1..10,会生成包含在从1到10中的一系列值。通常当你使用Loop component时,是在迭代整个一个List或者Collection的值,好比一次数据库查询的结果集。
所以,Loop component会将current属性设置为1,而后渲染其正文(就是<li>标记,还有ActionLink component)。而后它会将current属性设置为2而后再次渲染其正文……一直到10。
还要注意的是咱们正在使用ActionLink component;如今它再也不足够了解用户在ActionLink上的点击操做……咱们须要了解用户点击的是哪次迭代输出的连接。Context参数可让一个值被添加到ActionLink的URL之上,而咱们则能够在事件处理方法中获得这个值。
ActionLink的值将会是 /tutorial1/guess.makeguess/3。就是page的名称,“Guess”,component的id,“makeGuess”,还有上下文的值,“3”。
Guess.java(修订版)
package com.example.tutorial1.pages;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
public class Guess
{
@Property
@Persist
private int target, guessCount;
@Property
private int current;
void setup(int target)
{
this.target = target;
guessCount = 1;
}
void onActionFromMakeGuess(int value)
{
guessCount++;
}
}
Guess的修订版本包含两个新的属性:current和guessCount。还有一个来自于makeGuess ActionLink component的动做事件的处理器;当前它只是累加这个计数而已。
注意onActionFromMakeGuess()方法如今有了一个参数:这个参数就是被ActionLink编码到URL中的上线文的值。当用户点击了连接时,Tapestry会自动从URL获取到字符串,将其转换为一个int并将这个int传递给事件处理器方法。并不要你写多余的什么代码。
到此,page有了部分的可操做性:
下一步就是实际去检查用户提供的值是否跟目标值匹配,并提供反馈信息:无非就是踩得高了,低了或者对了。若是猜对了,咱们会切换到GameOver page,附上一条消息,好比“You guessed the number 5 in 2 guesses”。
咱们先从 Guess page开始,如今须要的是一个新的属性,用来存储将展现给用户的消息,还须要一个属性域注入 GameOver page:
Guess.java(局部)
@Property
@Persist(PersistenceConstants.FLASH)
private String message;
@InjectPage
private GameOver gameOver;
第一步完后,咱们会看到@Persist注解有了一些变化,其中由名称提供了一个持久化的策略。FLASH是一种内置的在会话中储值的策略,而只用于一个请求……它是为这类反馈消息而特别被设计出来的。若是你在浏览器中敲击键盘上的F5来刷新,page会被渲染而消息会消失。
接下来,再onActionFromMakeGuess()事件处理器方法中咱们须要更多的逻辑:
Guess.java(局部)
Object onActionFromMakeGuess(int value)
{
if (value == target)
{
gameOver.setup(target, guessCount);
return gameOver;
}
guessCount++;
message = String.format("Your guess of %d is too %s.", value,
value < target ? "low" : "high");
return null;
}
再一次,很是直接的。若是值是正确的,那么咱们会配置好GameOver page并返回它,导致page发生重定向。不然,咱们会累加猜想的次数,并格式化输出一条消息展现给用户。
在模板中,咱们只须要增长一些标记来展现消息就好了。
Guess.tml(局部)
<strong>Guess number ${guessCount}</strong>
<t:if test="message">
<p>
<strong>${message}</strong>
</p>
</t:if>
这块代码使用了Tapestry的if component。If component会计算器 test 参数,而若是其值被计算出来为true的话,就渲染其正文。被绑定到test的属性没必要是一个boolean;Tapestry会将null当作是false,将零当作是false而非零当作是true,它会将空的Collection当作是false……而对于String(好比message),它会将空字符串(那种为null,或者只有一些空格的)当作是false,而非空字符串当作是true。
咱们用“GameOver”page来收官了:
GameOver.java
package com.example.tutorial1.pages;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
public class GameOver
{
@Property
@Persist
private int target, guessCount;
void setup(int target, int guessCount)
{
this.target = target;
this.guessCount = guessCount;
}
}
GameOver.tml
<html t:type="layout" title="Game Over"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
xmlns:p="tapestry:parameter">
<p>
You guessed the number
<strong>${target}</strong>
in
<strong>${guessCount}</strong>
guesses.
</p>
</html>
结果是当你猜对了的时候,应该是这样的:
如上这些包含了Tapestry的一些基础知识;咱们已经展现了将page连接到一块儿以及用代码将信息在page之间传递,还有将数据融入URL的基础知识。
这个玩具应用程序还有重构的余地;例如,使其从GameOver page处开始一个新的游戏(而且要以代码不会重复的方式)成为可能。此外,稍后咱们会见到其它的在page之间共享信息的方式,比起这里展现的设置并持久化的方法少了些笨重。
接下来:让咱们看看Tapestry如何处理HTML表单和用户输入。