Tapestry 教程(五)實現Hi-Lo猜謎遊戲

咱們開始來構建一個基礎的Hi-Lo猜謎遊戲。html

在這個遊戲中,計算機會選擇一個介於110之間的數字。你嘗試猜出這個數字,點擊一些連接。最後,計算器會告訴你確認目標數字你須要猜多少次。即便是像這樣一個簡單的示例,也能體現Tapestry中的幾個重要概念:java

將一個應用程序分段放到各自獨立的幾個pageweb

將信息從給一個page傳送到另一個page數據庫

響應用戶的交互apache

在服務器端session中存儲客戶端信息編程

咱們將用幾個小塊的來構建這個小巧的應用程序,使用Tapestry來進行這種迭代式的開發很是容易。api

頁面流程很是簡單,包含三個page:Index(起始page),Guess以及GameOver。Index page對應用程序進行介紹,幷包含一個開始猜謎遊戲的連接。Guess page像用戶顯示10個連接,加上一些諸如「too low」,「too high」的提示信息。GameOver page告訴用戶在找到目標數字以前他們已經猜想了多少次。瀏覽器

Index 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。讓咱們來想一想當用戶點擊這個連接時應該要發生些什麼:

會有一個介於110之間的隨機數據被選出來

花費的猜想次數應該被重置爲0

用戶應該被指引至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」)和componentid(「start」)。

如今若是你點擊連接,頁面會顯示一個錯誤:

Tapestry要告訴咱們的是須要爲這個事件提供某種類型的事件處理器。這是個什麼東西呢?

事件處理器就是Java類中的一個帶有特殊名稱的方法。方法的形式如 onEventnameFromComponent-id……這裏咱們想要的是一個叫作onActionFromStart()的方法。那咱們是如何知道「action」纔是正確的事件名稱的呢?由於ActionLink就是這麼規定的,這也是爲何它被命名爲ActionLink的緣由。

Tapestry再一次爲咱們提供了選擇的餘地;若是你不喜歡約定的命名方式,能夠把@OnEvent註解放在方法的前頭,它能給予你按本身喜愛命名方法的自由。有關於此的詳細信息,見Tapestry的用戶指南。本教程咱們仍是堅持使用約定命名的方式吧。

在處理一個component事件請求(由ActionLink componentURL觸發的請求類型)時,Tapestry將會找到這個component並在其上觸發一個component 時間。這是咱們服務器端的代碼所須要的回調,用來了解用戶正在客戶端上面作些什麼。讓咱們先從一個空的事件處理器開始:

 

Index.java

package com.example.tutorial1.pages;

 

public class Index

{

    void onActionFromStart()

    {

 

    }

}

 

在瀏覽器中咱們能夠經過點擊刷新按鈕的操做來從新嘗試一下剛剛失敗的component 事件請求……或者咱們也能夠重啓應用程序。兩種狀況下,咱們看到的時候默認的行爲效果,就是簡單地從新渲染了一下page

注意事件處理方法並沒必要得是public的;它也能夠是protectedprivate或者package private(如這個示例)的。根據約定,這樣的方法都應該是package private的,如沒有其它理由那麼最少許的字符輸入量就是依據了。

呃……目前只能對於咱們會讓方法獲得調用這一點保持信任。這不怎麼好……什麼能快速的讓咱們確認這件事情呢?一種方法就是讓方法拋出異常,不過這有點笨啊。

那麼這樣如何:向方法添加一個@Log註解:

 

Index.java(局部)

import org.apache.tapestry5.annotations.Log;

 

. . .

 

    @Log

    void onActionFromStart()

    {

 

    }

 

接下來在你點擊連接時,就會在Eclipseconsole面板中看到以下信息:

 

[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 pageURL)。後續咱們會回過頭來扯這個。

咱們已經準備好進行下一步了,涉及到將IndexGuess 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;

 

    @Log

    Object onActionFromStart()

    {

        int target = random.nextInt(10) + 1;

 

        guess.setup(target);

        return guess;

    }

}

 

如今新的事件處理方法能夠選擇目標數字了,而且會告訴Guess page這個事情。由於Tapestry是一個被管理起來的環境,因此咱們不用建立Guess的一個實例……管理Guess page的生命週期是Tapestry該管的事情。所以,咱們該找Tapestry去要Guess page,就使用@InjectPage 註解。

Tapestrypage或者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爲你編寫gettersetter方法。你僅僅只須要在你準備在模板用引用這個屬性域時才這樣作。

咱們已經很是接近了,不過還有最後一個大的事情要處理。當你刷新了頁面,你會看到target變成了0

以前提過,Tapestry會在處理完事件請求以後發送給客戶端一個重定向。這意味着頁面的渲染髮生在一個全新的請求之中。同時,每一個請求的最後,Tapestry都會將每一個實例變量的值擦除。所以這就意味着在component 事件請求期間target是一個非0的數字……但有時若是從網頁瀏覽器處進來一個新的page渲染請求要渲染Guess page,那target屬性域的值就會回到其默認的0

這裏的解決方案就是標記出其值應該在從一個請求到接下來的請求(再接下來、再接下來……)中持續存在。這就是@Persist註解的由來了:

 

@Property 

@Persist

private int target;

 

這跟數據庫的持久化(這在以後的章節中會講到)一毛錢的關係都木有。只是意在讓值存儲在請求之間共享的HttpSession中。

回到Index page並再次點擊連接。最後,咱們有了一個目標數字:

對於咱們的起步階段這就夠了。讓咱們把Guess page 整出來,讓用戶能夠作猜想。咱們將顯示猜想的次數,而且在他們作猜想的時候讓次數累加。以後咱們要關注猜想是高了仍是低了,或者已經選擇了正確的值。

在構建Tapestrypage時,你有時會先從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屬性的。

咱們也看到了一個新的componentLoop componentLoop component會迭代傳入其source參數的值,對於每一個值都在其正文中渲染一遍。在渲染其正文以前,它會更新其value參數。

這個特殊的屬性表達式 1..10,會生成包含在從110中的一系列值。通常當你使用Loop component時,是在迭代整個一個List或者Collection的值,好比一次數據庫查詢的結果集。

所以,Loop component會將current屬性設置爲1,而後渲染其正文(就是<li>標記,還有ActionLink component)。而後它會將current屬性設置爲2而後再次渲染其正文……一直到10

還要注意的是咱們正在使用ActionLink component;如今它再也不足夠了解用戶在ActionLink上的點擊操做……咱們須要瞭解用戶點擊的是哪次迭代輸出的連接。Context參數可讓一個值被添加到ActionLinkURL之上,而咱們則能夠在事件處理方法中獲得這個值。

ActionLink的值將會是 /tutorial1/guess.makeguess/3。就是page的名稱,「Guess」,componentid,「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的修訂版本包含兩個新的屬性:currentguessCount。還有一個來自於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>

 

這塊代碼使用了Tapestryif componentIf component會計算器 test 參數,而若是其值被計算出來爲true的話,就渲染其正文。被綁定到test的屬性沒必要是一個booleanTapestry會將null當作是false,將零當作是false而非零當作是true,它會將空的Collection當作是false……而對於String(好比message),它會將空字符串(那種爲null,或者只有一些空格的)當作是false,而非空字符串當作是true

咱們用「GameOverpage來收官了:

 

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表單和用戶輸入。

接下來是:使用BeanEditForm來建立用戶表單

相關文章
相關標籤/搜索