创建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。
- 使用XDoclet创建 pageForm.html
- 创建 PersonFormTest 以测试 PersonForm
- 创建 PersonForm
- 运行 PersonFormTest
- 在你的浏览器中参看刚创建的PersonForm
- 创建 Canoo WebTests 以模拟测试 PesonForm 对浏览器中操作的响应
在这一步,你将自动生成一个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=IdpersonForm.firstName=First NamepersonForm.lastName=Last Nameperson.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 ListpersonList.heading=Persons# -- person detail page --personDetail.title=Person DetailpersonDetail.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 } 要为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 = (PersonForm) getPage(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 = (Manager) ctx.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 = (MockRequestCycle) page.getRequestCycle(); cycle.addServiceParameter(new Long(1)); page.edit(cycle);
assertNotNull(page.getPerson()); assertFalse(page.hasErrors()); } public void testSave() { assertNotNull(manager); Person person = (Person) manager.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 。
在 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 = (MainMenu) cycle.getPage("mainMenu"); nextPage.setMessage(getMessage("person.deleted")); cycle.activate(nextPage); }
public void edit(IRequestCycle cycle) { Object[] parameters = cycle.getServiceParameters(); Long id = (Long) parameters[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 = (MainMenu) cycle.getPage("mainMenu"); nextPage.setMessage(getMessage(key)); cycle.activate(nextPage); } else { PersonForm nextPage = (PersonForm) cycle.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 可以发现所有的测试依赖于数据库 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
现在执行
ant db-load deploy,启动 Tomcat 在浏览中输入
http://localhost:8080/appfuse/personForm.html ,你将看到如下的界面(略):
在 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="..."/> 加入所需要显示的信息。
最后一步(可选步骤)是创建一个
Canoo WebTest 测试这个 HTML 模板。
- 我之所以说着步骤是可选的,是因为你可以通过浏览器实现同样的操作。
你可以使用下面的步骤测试adding、editing 和 saving操作。
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纪录。
Trackback: http://tb.donews.net/TrackBack.aspx?PostId=255120