本教程中,我們會涉及到 Tapestry forms 的一些基礎知識,然後再通過一個 Tapestry 應用來加深理解。我們會看看如何在頁面之間傳遞信息。

這份教程的主題是一個用於追蹤 Tapestry 的第三方程序庫的應用程序。每個項目都有這些屬性:

  • name
  • release ID
  • short and long description
  • category
  • supported Tapestry version
  • release date
  • public (visible to others) or private (visible only to the owner)

稍後的教程將會回到這個主題上,還會談到關於數據庫存取的問題。在本教程中,我們會將上述的數據集合(在 AddProject 的頁面中)然後顯示出來。

Home Page

我們應用程序的主頁非常簡單,並不需要 Java 類:

<html jwcid="@Shell" title="Tapestry Component Database">
<body>
<h1>Tapestry Component Database</h1>

<p>
  Options:
</p>

<ul>
  <li><a jwcid="@PageLink" page="AddProject">Add New Project</a></li>
</ul>

</body>
</html>

我們介紹另一個很好用的組件:Shell。這個組件是為了方便生成 <html>、<head> 以及 <title> 標籤(雖然可生成的標籤不多)。

Value Object

Tapestry 其中一個基石就是直接編輯 value 對象屬性的能力。在一個真正的應用程序中,這些 value 對象從數據庫取出,由 Tapestry 組件編輯,然後返回到數據庫中。

與頁面不同,這種由 Tapestry 編輯對象的方法並不需要什麼條件,它們不必繼承某個基類或是實現某個接口。它們是 MVC 模式中真正的 Model。

在本教程中,我們會用到一個非常簡單的對象:

package tutorial.forms.data;

import java.util.Date;

/**
* Contains the name and description of a release of a project.
*
* @author Howard M. Lewis Ship
*/
public class ProjectRelease {

    private String name;
    private String releaseId;
    private String shortDescription;
    private String longDescription;
    private String category;
    private String tapestryVersion;
    private Date releaseDate;
    private boolean public;

    /**
     * A user-specified category, used to group similar projects.
     */
    public String getCategory() {
        return this.category;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    /**
     * A longer description used on a detail page.
     */
    public String getLongDescription() {
        return this.longDescription;
    }

    public void setLongDescription(String longDescription) {
        this.longDescription = longDescription;
    }

    /**
     * The name of the project.
     */
    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * If true, the project is visible to other users. If false, then the project is not visible.
     * This is used as a "draft" mode, when information about the project is not complete.
     */
    public boolean isPublic() {
        return this.public;
    }

    public void setPublic(boolean public) {
        this.public = public;
    }

    /**
     * The date when the project was released. Used to generate a chronological listing.
     */
    public Date getReleaseDate() {
        return this.releaseDate;
    }

    public void setReleaseDate(Date releaseDate) {
        this.releaseDate = releaseDate;
    }

    /**
     * The version number of the project that was released.
     */
    public String getReleaseId() {
        return this.releaseId;
    }

    public void setReleaseId(String releaseId) {
        this.releaseId = releaseId;
    }

    /**
     * A single-line description used in an overview listing.
     */
    public String getShortDescription() {
        return this.shortDescription;
    }

    public void setShortDescription(String shortDescription) {
        this.shortDescription = shortDescription;
    }

    /**
     * The version of Tapestry required for the project.
     */
    public String getTapestryVersion() {
        return this.tapestryVersion;
    }

    public void setTapestryVersion(String tapestryVersion) {
        this.tapestryVersion = tapestryVersion;
    }
}

AddProject Page

正如我們所見,主頁包含了一個 AddProject 頁面的鏈接;AddProject 頁面包含了一個收集項目信息的表單(這些信息會提交數據庫存儲)。在這個例子裡沒有數據庫,但我們還是可以收集這些信息。

HTML Template

<html jwcid="@Shell" title="Add New Project">
  <body jwcid="@Body">
    <h1>Add New Project</h1>
    <form jwcid="form@Form" success="listener:doSubmit">
      <table>
        <tr>
          <th>Name</th>
          <td>
            <input jwcid="name@TextField" value="ognl:project.name" size="40"/>
          </td>
        </tr>
        <tr>
          <th>Release ID</th>
          <td>
            <input jwcid="release@TextField" value="ognl:project.releaseId" size="20"/>
          </td>
        </tr>
        <tr>
          <th>Short Description</th>
          <td>
            <input jwcid="short@TextField" value="ognl:project.shortDescription" size="40"/>
          </td>
        </tr>
        <tr>
          <th>Long Description</th>
          <td>
            <textarea jwcid="long@TextArea" value="ognl:project.longDescription" rows="10" cols="40"/>
          </td>
        </tr>
        <tr>
          <th>Tapestry Version</th>
          <td>
            <input jwcid="tapestryVersion@TextField" value="ognl:project.tapestryVersion" size="20"/>
          </td>
        </tr>
        <tr>
          <th>Release Date</th>
          <td>
            <input jwcid="releaseDate@DatePicker" value="ognl:project.releaseDate"/>
          </td>
        </tr>
        <tr>
          <th>Public</th>
          <td>
            <input jwcid="public@Checkbox" value="ognl:project.public"/>
          </td>
        </tr>
      </table>
      <input type="submit" value="Add Project"/>
    </form>
  </body>
</html>

這個模板引入了一些新的組件:

  • Body -- 生成頁面的 JavaScript(需要使用 DatePicker
  • Form -- 生成 HTML 表單並控制 submit 的行為
  • TextField -- 創建用於編輯(讀取及更新)屬性的 text field(<input type="text" />)
  • TextArea -- 同 TextField,但生成的是多行的 <textarea>
  • DatePicker -- 彈出式的 JavaScript 日曆
  • Checkbox -- 編輯頁面的 boolean 屬性

表單中的 success 參數連接到監聽器函數上。只有在驗證通過後才會調用監聽器。我們會在稍後的教程中討論到信息的驗證。

Body 組件在 Tapestry 中扮演著一個重要的角色;它在頁面輸出時負責組織所有的 JavaScript。它幫助組件為客戶端的變量和函數生成唯一的名稱,將所有組件在頁面中生成的 JavaScript 分成兩大塊(一塊在頁面的頂部,一個在底部)。DatePicker 組件在由 Body 組件封裝之前不會進行工作。

TextFieldTextArea 用於編輯頁面的屬性。因為 value 參數是 OGNL 表達式,所以編輯由頁面類直接暴露的屬性並不是限制,這樣可以跟蹤屬性路徑。我們馬上會看到怎樣定義項目頁面的屬性。

正如你所見,Tapestry 提供了一些用於編輯指定屬性類型的組件。另外,我們還會談談如何配置一個既有的組件讓它可以編輯其它的類型。

AddProject Page Class

我們從一個最小的類開始,然後在必要時加入一些細節。

package tutorial.forms.pages;

import org.apache.tapestry.html.BasePage;
import tutorial.forms.data.ProjectRelease;

/**
* Java class for the AddProject page; contains
* a form used to collect data for creating a new

* {@link tutorial.forms.data.ProjectRelease}.
*
* @author Howard M. Lewis Ship

*/
public abstract class AddProject extends BasePage {

    public abstract ProjectRelease getProject();

    public void doSubmit() {
    }
}

或許這個類實在是太小了;如果我們運行程序然後點擊 Add New Project 鏈接會出現一個異常:

forms1

這個異常源於一個空值:我們定義了存儲 ProjectRelease 對象的地方但並實際上沒有提供它的實例。OGNL 企圖解除對這個空值的引用然後擲出 OgnlException。這裡我們可以看到 Tapestry 異常報告的好處:它顯示的堆棧異常信息給出了應用程序的 context(模板中出錯的行)而沒有晦澀難懂的信息。

我們需要做的是創建 ProjectRelease 的實例然後將它存儲在屬性中,好讓 TextField 組件可以編輯它。我們得非常小心因為在一個運行的應用中,頁面會被置於緩衝池中然後被不斷的複用。

對於這種情況,正確的做法是監聽 PageBeginRender 事件,接著將新的實例存儲到屬性中。ProjectRelease 對象會在 request 期間使用,然後於 request 的最後丟棄。

監聽這些生存期事件非常簡單;你只需要選擇一個適當的 listener 接口然後實現它;Tapestry 就會自動為你的頁面進行註冊以接收通知。本例的接口是 PageBeginRenderListener

package tutorial.forms.pages;

import java.util.Date;

import org.apache.tapestry.event.PageBeginRenderListener;
import org.apache.tapestry.event.PageEvent;
import org.apache.tapestry.html.BasePage;

import tutorial.forms.data.ProjectRelease;

/**
* Java class for the AddProject page; contains a form used to collect data for creating a new
* {@link tutorial.forms.data.ProjectRelease}.
*
* @author Howard M. Lewis Ship
*/
public abstract class AddProject extends BasePage implements PageBeginRenderListener {

    public abstract ProjectRelease getProject();

    public abstract void setProject(ProjectRelease project);

    public void pageBeginRender(PageEvent event) {
        ProjectRelease project = new ProjectRelease();
        project.setReleaseDate(new Date());
        setProject(project);
    }

    public void doSubmit() {
    }
}

每當頁面輸出時,pageBeginRender() 函數就會被調用。同樣的,當頁面中的表單提交時調用函數,這得歸功於 Tapestry 中一個有用的小功能。我們不僅可以創建實例,而且還可以為數據域設值。

有了這個類,頁面就可以輸出了,然後我們可以輸入一些數據:

forms2

Form Submission

上面的實現中,提交表單似乎並沒有做些什麼。當然,表單提交,然後信息就從 request 中取出然後存儲到 ProjectRelease 對象的屬性中,但接著 AddProject 頁面就被重新輸出了。

那麼,怎樣保留 ProjectRelease 對象全得當表單提交、TextField 組件更新 project.name 屬性時我們不會再出現 NullPoinerException 呢?當 ProjectRelease 對象被丟棄時都發生了什麼?但當頁面的表單被提交時,PageBeginRender 接口就又會被觸發,就像一個表單輸出器一樣。這意味著既有的代碼確實創建了一個新的 ProjectRelease 實例,使得 TextField 可以存儲來自於表單的值。

同樣棒的是新的 ProjectRelease 對象更新了,我們需要的是發生些什麼。我們將要做出一些更動好讓表單提交時顯示不同的頁面。進一步的,表單會顯示由 AddProject 頁面收集而來的同樣的信息。

要完全這個,我們需要改動 doSubmit() 函數,讓它獲得 ShowProject 頁面。讓不同的頁面協同工作最簡單的辦法就是將一個頁面注入另一個頁面中。這可以用 annotation 來完成:

    @InjectPage("ShowProject")
    public abstract ShowProject getShowProject();

這段 AddProject.java 中的代碼,建立了頁面 AddProject 與 ShowProject 之間的連接。

我們可以用 doSubmit() 中的代碼將信息從頁面 AddProject 傳遞到 ShowProject;也可以激活 ShowProject 頁面讓它作出響應。

public IPage doSubmit() {
    ShowProject showProject = getShowProject();
    showProject.setProject(getProject());
    return showProject;
}

這小段代碼中有許多值得注意的地方。第一,函數不再是 void,它返回一個頁面(IPage 是所有頁面類都要實現的接口)。當 listener 函數返回一個頁面時,該頁面就成為了回應客戶端的有效頁面。

當我們談到對象、函數和屬性時,指得就是這個例子。我們沒有說到“ShowProject.html& amp; rdquo;模板,我們說的是 ShowProject 頁面,並沒有談到它的模板所在及模板裡都有些什麼。進一步的,要將信息從頁面 AddProject 傳遞到 ShowProject 上,我們並不需要和 HttpServletRequest 屬性攪和:我們將對象存儲在 ShowProject 頁面的屬性中。

萬事具備,我們現在可以提交表單然後看看 ShowProject 頁面了:

forms3

Annotation