临海观潮

个人编程、项目管理经验、感想。

博客园 首页 新随笔 联系 订阅 管理

创建Tapestry框架页面

第三部分: 创建 Tapestry page 和 HTML 模版 - 介绍如何在AppFuse 项目中创建 Tapestry页面和模版。

这个指南依赖于 第二部分: 创建新的 Managers 对象

说明

这个指南将会告诉你如何创建 Tapestry 页面和 HTML 模版。同时也将说明如何编写 JUnit 测试以测试PersonForm页面。我们创建的这个JSP页面将会使用我们在”创建Managers类“ 指南创建的PersonManager类。在大多数的web框架(web frameworks)中,控制逻辑都写在一个类似"Action" 的类中。但是在Tapestry中,这些控制逻辑通常可以以类似"Page"的方式被引用。使用这些pages的方法被称为listeners。这份指南不会讲述关于Tapestry工作机制的问题,但是会知道你快速上手使用Taperstry框架。如果你希望跟深入的学习Taperstry的有关知识,我建议你阅读Howard Lewis Ship'的 Tapestry in Action一书。当我在把Tapersty集成到Appfuse的过程中就把这部书放在身边以便随时查阅。感谢Howard的帮助!

我将以斜体字说明在 实际过程 使用的经验。

现在让我们开始在Appfuse's的整体架构下创建新的页面和HTML模版。如果你此时还没有安装Tapestry模块,请马上运行ant install-tapestry

内容提要

  • [1] 使用XDoclet创建 pageForm.html
  • [2] 创建 PersonFormTest 以测试 PersonForm
  • [3] 创建 PersonForm
  • [4] 运行 PersonFormTest
  • [5] 在你的浏览器中参看刚创建的PersonForm
  • [6] 创建 Canoo WebTests 以模拟测试 PesonForm 对浏览器中操作的响应

使用XDoclet创建 pageForm.html 模版[#1]

在这一步,你将自动生成一个HTML模版以显示Person对象的信息。这个模版将包含符合Taperstry语法规则的待填充表单元素 - 就是那些HTML文件中带有"jwcid" 的属性。用来自动生成HTML模版的AppGen工具是基于 StrutsGen工具实现的 - 这个工具最初是由Erik Hatcher开发的。基本上是由一对Java类和一组XDoclet模版组成。这些文件都可以在extras/appgen目录下找到。

下面给出产生这个HTML模版文件和一个包含了form的标签元素的properties文件的具体步骤:

  • 在命令行环境下,切换到 "extras/appgen" 目录
  • 执行 ant -Dmodel.name=Person -Dmodel.name.lowercase=person 将在extras/appgen/build/gen目录下产生这组文件。事实上,它将产生你在完成这个向导所用到的所有文件。不过,我们现在仅仅抓住那些你需要的那些文件。
    • web/WEB-INF/classes/Person.properties (表单元素的标签[label]字符串)
    • web/pages/PersonForm.html (显示一个Person对象信息的HTML 模版文件)
    • web/pages/PersonForm.page (说明前一页面的Page)
    • web/pages/PersonList.html (显示一个People列表的HTML 模版文件)
    • web/pages/PersonList.page (说明前一页面的Page)
  • 把Person.properties文件中的内容拷贝到web/WEB-INF/classes/ApplicationResources_en.properties文件中。这些都是你在JSP页面中用来显示的titles/headings 和form属性对应的具体键-值对。下面是你需要在ApplicationResources_en.properties文件中新增的内容示例:
# -- person form --
personForm.id=Id
personForm.firstName=First Name
personForm.lastName=Last Name

person.added=Person has been added successfully.
person.updated=Person has been updated successfully.
person.deleted=Person has been deleted successfully.

# -- person list page --
personList.title=Person List
personList.heading=Persons

# -- person detail page --
personDetail.title=Person Detail
personDetail.heading=Person Information
  • 拷贝PersonForm.html 和 PersonForm.page 文件 到 web/pages/personForm.jsp 和 web/pages/personForm.page文件。拷贝PersonList.html和PersonList.page文件到web/pages/personList.jsp和web/pages/personList.page。请注意目标文件名的第一个字符是小写。

在 "pages" 目录下的文件将被罚不到"WEB-INF/pages"目录下。因为容器会为WEB-INF目录下的文件提供安全性保护。这意味着直接来自客户端的请求,而不是由 Tapestry's ApplicationServlet 发送的 forward请求,将无法访问对应的 JSP 页面。就是说把所有的 HTML 模版放在 WEB-INF 目录下将保证所有的 JSP 页面只能通过Taperstry的 Pages 来访问。这就允许把所有的安全性处理集中放到 Taperstry Page 中去,在那里可以得到更有效的处理,而不在需要在表示层中处理。

Appfsue的web应用程序安全性保证所有 *.html url-patterns 都是有保护的 (除了 /signup.html 和 /passwordHint.html), 这将保证客户端必须通过Page(Tperstry 框架页面条专逻辑控制器对应的 Page )来访问 template 文件。

注意: 如果你希望为特别的页面自定义一个CSS,你可以在这个文件的最上方加入 <body id="pageName"/> 标签 (紧跟在</content> 标签后面)。 SiteMesh会特别处理并把它放在最终的页面中。你也可用使用如下的代码一个页面一个页面的自定义CSS:
body#pageName element.class { background-color: blue } 

创建PersonFormTest以测试[#2]

要为PersonForm创建一个 Junit 测试,首先在 test/web/**/action 目录下创建一个 PersonFormTest.java 文件。


package org.appfuse.webapp.action;

import java.util.ResourceBundle;

import org.appfuse.model.Person;
import org.appfuse.service.Manager;

public class PersonFormTest extends BasePageTestCase {
    private PersonForm page;
    private Manager manager;

    protected void setUp() throws Exception {    
        super.setUp();
        page = (PersonFormgetPage(PersonForm.class);
        // unfortunately this is a required step if you're calling 
        // getMessage in the page class
        page.setBundle(ResourceBundle.getBundle(MESSAGES));
        page.setValidationDelegate(new Validator());

        // this manager can be mocked if you want a more "pure" unit test
        manager = (Managerctx.getBean("manager");
        page.setManager(manager);
        // default request cycle
        page.setRequestCycle(getCycle(request, response));
    }

    protected void tearDown() throws Exception {
        super.tearDown();
        page = null;
    }

    public void testAdd() throws Exception {
        Person person = new Person();
        // set required fields
        person.setFirstName("firstName");
        person.setLastName("lastName");
        page.setPerson(person);

        page.save(page.getRequestCycle());
        assertFalse(page.hasErrors());
    }

    public void testEdit() throws Exception {
        MockRequestCycle cycle = (MockRequestCyclepage.getRequestCycle();
        cycle.addServiceParameter(new Long(1));
        
        page.edit(cycle);

        assertNotNull(page.getPerson());
        assertFalse(page.hasErrors());
    }
    
    public void testSave() {
        assertNotNull(manager);
        Person person = (Personmanager.getObject(Person.class, new Long(1));

        // update fields
        person.setFirstName("firstName");
        person.setLastName("lastName");
        page.setPerson(person);

        page.save(page.getRequestCycle());
        assertFalse(page.hasErrors());
    }

    public void testRemove() throws Exception {
        Person person = new Person();
        person.setId(new Long(2));
        page.setPerson(person);

        page.delete(page.getRequestCycle());
        assertFalse(page.hasErrors());
    }
}

此时将不能通过编译因为你还没有创建被测试的 PersonForm 。

创建 PersonForm [#3]

在 src/web/**/action 目录下创建 PersonForm.java 文件,输入下面的内容:


package org.appfuse.webapp.action;

import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.event.PageEvent;
import org.apache.tapestry.event.PageRenderListener;

import org.appfuse.model.Person;
import org.appfuse.service.PersonManager;

public abstract class PersonForm extends BasePage implements PageRenderListener {
    public abstract PersonManager getPersonManager();
    public abstract void setPersonManager(PersonManager mgr);
    public abstract void setPerson(Person person);
    public abstract Person getPerson();

    public void pageBeginRender(PageEvent event) {
        if ((getPerson() == null&& !event.getRequestCycle().isRewinding()) {
            setPerson(new Person());
        else if (event.getRequestCycle().isRewinding()) { // add
            setPerson(new Person());
        }
    }

    public void cancel(IRequestCycle cycle) {
        if (log.isDebugEnabled()) {
            log.debug("Entering 'cancel' method");
        }

        cycle.activate("mainMenu");
    }

    public void delete(IRequestCycle cycle) {
        if (log.isDebugEnabled()) {
            log.debug("entered 'delete' method");
        }

        getPersonManager().removePerson(getPerson().getId().toString());

        MainMenu nextPage = (MainMenucycle.getPage("mainMenu");
        nextPage.setMessage(getMessage("person.deleted"));
        cycle.activate(nextPage);
    }

    public void edit(IRequestCycle cycle) {
        Object[] parameters = cycle.getServiceParameters();
        Long id = (Longparameters[0];
        
        if (log.isDebugEnabled()) {
            log.debug("getting person with id: " + id);
        }
        
        setPerson(getPersonManager().getPerson(id.toString()));
        cycle.activate(this);
    }
    
    public void save(IRequestCycle cycle) {
        if (getValidationDelegate().getHasErrors()) {
            return;
        }

        boolean isNew = (getPerson().getId() == null);

        getPersonManager().savePerson(getPerson());

        String key = (isNew"person.added" "person.updated";

        if (isNew) {
            MainMenu nextPage = (MainMenucycle.getPage("mainMenu");
            nextPage.setMessage(getMessage(key));
            cycle.activate(nextPage);
        else {
            PersonForm nextPage = (PersonFormcycle.getPage("personForm");
            nextPage.setMessage(getMessage(key));
            cycle.activate("personForm")// return to current page
        }
    }
}

你可能注意到在文件中使用了一组键(keys) - "person.deleted","person.added" 和 "person.updated"。所有的这些键值定义在你的 i18n 绑定文件(ApplicationResources_en.properties)中。你在这篇指南的开头应该已经添加了这部分内容。如果你希望在程序中改变这些基本信息,加入 person 的 name 或者其他内容,只需要在对应的信息内容中简单得添加一个 "{0}" 然后再程序中使用 setMessage(format(key, stringtoreplace)) 方法填充具体的内容。

你现在可能注意到了我们在这里调用 PersonManager 的代码和我们 PersonManagerTest 的相应代码是一样的。因为 PersonForm 和 PersonManagerTest 都是PersonManagerImpl 的客户 , 所以这是个优雅的结构。

现在你要告诉 Tapestry 这个 page 的存在了。你要做的是在 web/WEB-INF/tapestry.application 文件中加入 page 入口。


    <page name="personForm" specification-path="pages/personForm.page"/>

如果你把 HTML 模板文件保存在 WEB-INF 目录下,上面的步骤是不需要的。希望Taperstry未来的版本允许你设置全局路径。

这个从cancel(), delete() and save()方法 PersonForm 返回到 "MainMenu" 页面 。在下面的部分,你将把它改变成 PersonList 页面。

运行 PersonFormTest [#4]

你看 PersonFormTest 可以发现所有的测试依赖于数据库 person 表中一条 id=1 的纪录( testRemove 方法依赖于 id=2 的纪录 ),所以要在示例数据文件( metadata/sql/sample-data.xml )中加入这些纪录。我通常在文件的底部加入这些内容 - 这个顺序并不重要因为它和其他数据表没有任何关系。

  <table name='person'>
    <column>id</column>
    <column>first_name</column>
    <column>last_name</column>
    <row>
      <value>1</value>
      <value>Matt</value>
      <value>Raible</value>
    </row>
    <row>
      <value>2</value>
      <value>James</value>
      <value>Davidson</value>
    </row>
  </table>

在运行所有的测试以前 DBUnit 会加载这些数据到数据库中,所以这些纪录对你的 Form 测试是可靠的。

保证你的项目中的文件都正确保存。那样你运行ant test-web -Dtestcase=PersonForm - 所有的事情就像你最初期望的那样。

BUILD SUCCESSFUL
Total time: 12 seconds

在浏览器中查看这个表单[#5]

现在执行 ant db-load deploy,启动 Tomcat 在浏览中输入 http://localhost:8080/appfuse/personForm.html ,你将看到如下的界面(略):

注意: Tapestry 会自动把焦点设置在表单中第一个必须输入的字段中。如果希望改变焦点所在位置,可以查看 mailing list archives

在 Tapestry 中,URLs 显得有点丑陋,不过他们包含了大量的信息。与其他的框架只需要你简单调用Action中的方法不一样, - 你需要调用 Page 类中的 listeners 。为了调用PrsonForm 对象中的 "edit" listener ,需要在 web/pages/mainMenu.html 文件中加入下面的代码。

    <a jwcid="@DirectLink" listener="ognl:requestCycle.getPage('personForm').listeners.edit" 
        parameters="ognl:new java.lang.Long(1)">Edit Person</a>

最后为了提高界面的用户友好性,你也许希望在表单的上方加入信息,这可以在 personForm.html 中前面使用 <span key="..."/> 加入所需要显示的信息。

[Optional] 创建一个Canoo WebTest 以模拟测试 PesonForm 对浏览器中操作的响应[#6]

最后一步(可选步骤)是创建一个 Canoo WebTest 测试这个 HTML 模板。

我之所以说着步骤是可选的,是因为你可以通过浏览器实现同样的操作。

你可以使用下面的步骤测试adding、editing 和 saving操作。

  • Add - http://localhost:8080/appfuse/personForm.html.
  • Edit - 使用你在 Main Menu 中创建的 URL ( 确保事先运行过 ant db-load )。
  • Delete - 使用上面的 edit 链接点击 Delete 按钮。
  • Save - 点击 Main Menu 中的 edit 链接 ( 如果你已经删除了纪录需要再次运行 ant db-load ) 然后点击 Save 按钮。

Canoo 测试相当灵活,只需要通过在一个XML文件中配置实现。为了增加 add, edit, save 和 delete 操作的测试,打开 test/web/web-tests.xml 文件并且加入下面的XML。你可以看到一个命名为 PersonTests 目标的片断可以运行所有相关的测试。

我使用 CamelCase 命名 target ( 不同于传统的小写字母中线分割的命名方法 ) 因为你测试时要输入-Dtestcase=Name ,我发现我习惯使用 CamelCase 命名我的单元测试。


    <!-- runs person-related tests -->
    <target name="PersonTests"
        depends="EditPerson,SavePerson,AddPerson,DeletePerson"
        description="Call and executes all person test cases (targets)">
        <echo>Successfully ran all Person HTML Template tests!</echo>
    </target>

    <!-- Verify the edit person screen displays without errors -->
    <target name="EditPerson"
        description="Tests editing an existing Person's information">
        <canoo name="editPerson">
            &config;
            <steps>
                &login;
                <clicklink label="Edit Person"/>
                <verifytitle stepid="we should see the personDetail title"
                    text="${webapp.prefix}${personDetail.title}"/>
            </steps>
        </canoo>
    </target>

    <!-- Edit a person and then save -->
    <target name="SavePerson"
        description="Tests editing and saving a user">
        <canoo name="savePerson">
            &config;
            <steps>
                &login;
                <clicklink label="Edit Person"/>
                <verifytitle stepid="we should see the personDetail title"
                    text="${webapp.prefix}${personDetail.title}"/>
                <!-- update some of the required fields -->
                <setinputfield stepid="set firstName" name="firstNameField" value="Canoo"/>
                <setinputfield stepid="set lastName" name="lastNameField" value="WebTest"/>
                <clickbutton label="${button.save}" stepid="Click Save"/>
                <verifytitle stepid="Page re-appears if save successful"
                    text="${webapp.prefix}${personDetail.title}"/>
            </steps>
        </canoo>
    </target>

    <!-- Add a new Person -->
    <target name="AddPerson"
        description="Adds a new Person">
        <canoo name="addPerson">
            &config;
            <steps>
                &login;
                <invoke stepid="View Person Form" url="/personForm.html"/>
                <verifytitle stepid="we should see the personDetail title"
                    text="${webapp.prefix}${personDetail.title}"/>
                <!-- enter required fields -->
                <setinputfield stepid="set firstName" name="firstNameField" value="Jack"/>
                <setinputfield stepid="set lastName" name="lastNameField" value="Raible"/>
                <clickbutton label="${button.save}" stepid="Click button 'Save'"/>
                <verifytitle stepid="Main Menu appears if save successful" 
                    text="${webapp.prefix}${mainMenu.title}"/>
                <verifytext stepid="verify success message" text="${person.added}"/>
            </steps>
        </canoo>
    </target>

    <!-- Delete existing person -->
    <target name="DeletePerson"
        description="Deletes existing Person">
        <canoo name="deletePerson">
            &config;
            <steps>
                &login;
                <clicklink label="Edit Person"/>
                <clickbutton label="${button.delete}" stepid="Click button 'Delete'"/>
                <verifytitle stepid="display Main Menu" text="${webapp.prefix}${mainMenu.title}"/>
                <verifytext stepid="verify success message" text="${person.deleted}"/>
            </steps>
        </canoo>
    </target>

完成了前面的操作后,你可以在Tomcat运行的状态下运行 ant test-canoo -Dtestcase=PersonTests; 也可以在没有 Tomcat 运行的情况下运行 ant test-html -Dtestcase=PersonTests , Ant 会启动启动/停止 Tomcat。为了在运行所有 Canoo 测试的时候能够包括 PersonTests, 在"run-all-tests" target. 中加入对应的dependency。

你可能注意到Canoo测试没有客户端的日志记录。如果你想看看它到底做了什么,你可以在 web/WEB-INF/classes/log4j.properties 中加入 tweak the log4j settings

BUILD SUCCESSFUL
Total time: 27 seconds


下面的内容: 第四部分: 加入校验和列表页面 - 说明如何增加校验逻辑来使得 firstName 和 lastName 是必填字段。也将展示如何增加一个列表页面显示数据库中所有的person纪录。

posted on 2005-01-24 22:03  书生无用  阅读(1383)  评论(0编辑  收藏  举报