Tapestry 教程(六)使用BeanEditForm来创建用户表单

在前面一章,我们看到了Tapestry如何处理简单地链接,甚至于处理能在URL中传递信息的链接。在本章,我们将会看到Tapestry如何以不同的方式做同样的事情,以及相当多其它的事情,如HTML表单。

Tapestry中的表单支持深入而且丰富,以至于一个单独章节的内容还装不下。不过,我们可以展示一些基础的,包括一些非常普遍的开发模式。开始我们先来创建一个简单的地址簿应用程序。

先从实体数据着手,这是一个我们需要的用来存储信息的简单对象。这些类被放在entities子包下面。不同于pages子包(放component类的)的用法,这个子包并不受Tapestry的管束;只是一种约定(不过如我们很快就会发现的,这是比较方便的一个约定)而已。

Tapestry将公共的属性域当做是JavaBean的属性;因为Address 对象只是一些“呆数据dumb data”,所以没有必要费力去写什么getter和setter。而是完全把他们定义成public的属性域。

src/main/java/com/example/tutorial/entities/Address.java

package com.example.tutorial1.entities;

import com.example.tutorial1.data.Honorific;

public class Address

{

    public Honorific honorific;

    public String firstName;

    public String lastName;

    public String street1;

    public String street2;

    public String city;

    public String state;

    public String zip;

    public String email;

    public String phone;

}

我们还要定义这个枚举类型,Honorific:

src/main/java/com/example/tutorial/data/Honorific.java

package com.example.tutorial1.data;

public enum Honorific

{

    MR, MRS, MISS, DR

}

Address Page

我们可能要去创建一些同地址相关的page:创建地址的page、编辑地址的page、搜索和列出地址的page。我们将创建一个子文件夹,address,来放置他们。先从这些page的第一个开始,“address/Create”(这就是实际名称,包括斜线——稍后我们会明白它是如何映射到类和模板的)。

首先,我们将Index.tml更新,创建一个到新的page的链接:

src/main/resources/com/example/tutorial/pages/Index.tml (partial)

<h1>Address Book</h1>

<ul>

    <li><t:pagelink page="address/create">Create new address</t:pagelink></li>

</ul>

现在我们需要address/Create page;先从一个空壳开始,只测试导航是否是通的:

src/main/resources/com/example/tutorial/pages/address/CreateAddress.tml

<html t:type="layout" title="Create New Address"

    xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">

    <em>coming soon ...</em>

</html>

(注意:对于Tapestry5.4,用tapestry_5_4.xsd)

接下来是对应的类:

src/main/java/com/example/tutorial/pages/address/CreateAddress.java

package com.example.tutorial1.pages.address;

public class CreateAddress

{

}

那么……为什么类被命名为“CreateAddress”而不是简单的“Create”呢?实际上,我们可以把它命名为“Create”,应用程序仍然会运作,不过更长的类名称是同样可用的。Tapestry知道类名称(com.example.tutorial1.pages.address.CreateAddress)中多余的东西是什么意思,并且会截掉多余的后缀。

实际上Tapestry为你的page创建了一堆的别名;这些别名中的任何一个都是可以使用的,并且可以出现在URL或者PageLink的page 参数中。你可以在控制台中看到这个清单:

[INFO] TapestryModule.ComponentClassResolver Available pages (12):

              (blank): com.example.tutorial1.pages.Index

   ComponentLibraries: org.apache.tapestry5.corelib.pages.ComponentLibraries

             Error404: com.example.tutorial1.pages.Error404

      ExceptionReport: org.apache.tapestry5.corelib.pages.ExceptionReport

             GameOver: com.example.tutorial1.pages.GameOver

                Guess: com.example.tutorial1.pages.Guess

                Index: com.example.tutorial1.pages.Index

          PageCatalog: org.apache.tapestry5.corelib.pages.PageCatalog

PropertyDisplayBlocks: org.apache.tapestry5.corelib.pages.PropertyDisplayBlocks

   PropertyEditBlocks: org.apache.tapestry5.corelib.pages.PropertyEditBlocks

        ServiceStatus: org.apache.tapestry5.corelib.pages.ServiceStatus

          T5Dashboard: org.apache.tapestry5.corelib.pages.T5Dashboard

       address/Create: com.example.tutorial1.pages.address.CreateAddress

address/CreateAddress: com.example.tutorial1.pages.address.CreateAddress

Tapestry在构造URL时会使用最短的那个别名。

最终,你的应用程序可能会有更多的实体:可能你会有一个“user/Create”page和一个“payment/Create”page以及一个“account/Create”page。你可能会有一堆全部被命名为Create的不同的类,分布于许多不同的包中。这都是合法的Java,但并不理想。某一天你可能会突然发现正在编辑创建Account的Java代码,而你实际想要编辑的是创建Payment的代码。

因此Tapestry鼓励你使用更加具有描述性的名称。CreateAddress,而不仅仅只是Create,不过这并不需要你付出代价(比如更长更笨重的URL)。访问这个page的URL仍然是 http://localhost:8080/tutorial1/address/create

还要记得,不管Tapestry给你的page分配的名称是什么,模板文件的名称跟Java类一定是一样的,CreateAddress.tml。

Index page在文件夹中也能起作用。一个叫做com.example.tutorial1.pages.address.AddressIndex的类可能会被分配名称“address/Index”。然而,Tapestry对于叫做“Index”的page有特殊的对着,其渲染的URL可能会是 http://localhost:8080/tutorial1/address 。换言之,你可以将Index page放到任何文件夹中,而Tapestry将会为这个page构造一个简短的URL……而你不必一直将类命名为Index(让许多的类、设置跨多个包的类拥有同一个名字会令人迷惑);你可以将每个index page以其所在包来命名。Tapestry 使用一个聪明的约定来保持直接并生成出简短的URL。

使用BeanEditForm component

是时候以这种形式来将逻辑组合到一起了。Tapestry有一个用于客户端表单的特殊的component:Form component,以及用于表单控制的 component,比如 Checkbox 和 TextField。我们在稍后会涉及到它们……这里我们再一次通过BeanEditForm component让Tapestry为我们干了些体力活儿。

将如下代码添加到 CreateAddress 模板(替换“coming soon...”消息):

CreateAddress.tml (局部)

<t:beaneditform object="address"/>

并对应到CeateAddress类中的一个属性:

CreateAddress.java (局部)

@Property

private Address address;

当你刷新页面的时候,可能会在页面的顶部看到如下这样一条警告信息:

如果你看到了这个,就意味着你需要为应用创建一个HMAC密码。只要像下面这样编辑你的 AppModule.java 类(就在你的services 包中),添加几行代码到 contributeApplicationDefaulsts方法就行了:

// Set the HMAC pass phrase to secure object data serialized to client

configuration.add(SymbolConstants.HMAC_PASSPHRASE, "");

不过,不能是一个空的字符串,而是要插入一个长的,随机字符串(就像是一个非常长而且复杂的密码,至少30个字符),只有你自己知道。

在你做了这个之后,停掉应用并且重新启动它,然后在Create new address 链接上再次点击,就会看到像下面这样的效果:

Tapestry在这里做了许多的工作。它创建了一个表单,包含对应每个属性的输入域。不止如此,它还知道 honorific 属性是一个枚举类型,所以就以下拉列表输入框来呈现。

此外,Tapestry已经将属性名称(“city”,“email”,“firstName”)转换成显示给用户看的样子(“City”,“Email”,“First Name”)。事实上,它们都是<label>元素,因此用鼠标点击一个label会将输入指针移动到对应的输入域当中去。

这是一个很棒的开头;这是一个很具有表现力的界面,事实上几分钟的工作就能有一个相当不错的效果。不过同完美相比它还远远不够。让我们开始来做一些自定义吧。

修改输入域的顺序

BeanEditForm必须揣度这以正确的顺序呈现输入域,结果就是按照字母表的顺序来的。对于标准的JavaBean属性,BeanEditForm默认是以其getter方法在类中定义的顺序排列的(它使用了行号信息,如何可以获取到这个信息的话)。

排列这些输入域更好的顺序就是它们在Address类中被定义的顺序:

l honorific

l firstName

l lastName

l street1

l street2

l city

l state

l zip

l email

l phone

我们可以借助于BeanEditForm的 reorder 参数的使用来完成这样的排序,其值就是以逗号分隔的属性(或者公共域)名称的列表:

CreateAddress.tml(局部)

<t:beaneditform object="address"

    reorder="honorific,firstName,lastName,street1,street2,city,state,zip,email,phone" />

Label的自定义

Tapestry让自定义使用在输入域上的label变得相当的轻松。这是一个标准的Java properties文件,它被以跟page或者component类相同的名称命名,带有一个“.properties”扩展。一个消息清单包含了许多行,每一行都是一个消息键对应一条消息值,中间用等号分开。

其全部内容就是创建一个个带有特殊名称的消息词条:以“-label”为后缀的属性的名称。跟其它地方一样,Tapestry是不在意大小写的。

src/main/resources/com/example/tutorial/pages/address/CreateAddress.properties

street1-label=Street 1

street2-label=Street 2

email-label=E-Mail

zip-label=Zip Code

phone-label=Phone Number

因为这是一个新的文件(并不是对现有文件的修改),你可能要重启Jetty来强制让Tapestry获取到这个新的文件才行。

我们也可以对下拉列表框中的选项进行自定义。需要做的就是网消息清单中添加更多的词条,以将枚举名称匹配到想要的label上面。更新CreateAddress.properties,添加如下几行:

MR=Mr.

MRS=Mrs.

DR=Dr.

注意我们并不需要为MISS添加一个选项,因为无论如何它都会被转成“Miss”。你可能只是想为了一致性而把它加进来……关键是,每个选项的label是单独检索的。

最后,提交按钮的默认label是“Create/Update”(BeanEditForm并不知道它是被如何使用的)。让我们来把它改成“Create Address”。

<t:beaneditform submitlabel="Create Address" object="address"

    reorder="honorific,firstName,lastName,street1,street2,city,state,zip,email,phone"/>

提交的label默认为“Create/Update”,不过这里我们要将默认的覆盖成一个特殊的值。

最后的结果显示了重新调整的形式和label:

在继续进行验证之前,还有一个关于消息清单的点附带要注意下。消息清单不单单值用来重新设置输入域和选项的label,我稍后的章节中我们还可以看到消息清单是如何用于本地化和国际化的场景中的。

不把提交按钮的label直接放到模板里面,而是给label提供一个引用;实际的label将会被放在消息清单中。

在Tapestry中,每当要绑定一个参数,你所提供的值可能会包含一个前缀。前缀会指引Tapestry如何解释参数值中(除了前缀之外)的余下部分…它是不是一个属性的名称?是不是一个component的id?是不死消息的键?大多数参数都有一个默认的前缀,一般是“prop:”,在你没有提供一个前缀的时候就会用到(这有助于让模板尽可能的简洁)。

这里我们想要引用一条来自清单的消息,因此我们使用了“message:”前缀:

<t:beaneditform object="address" submitlabel="message:submit-label"

    reorder="honorific,firstName,lastName,street1,street2,city,state,zip,email,phone" />

然后我们要在消息清单中定义submit-label:

Submit-label=Create Address

最后,不管你是直接在模板中包含了label文本,还是间接地引用消息清单中的项,发送给客户端的HTML是一样的。长远看来,后者会在之后你要选择将应用程序进行国际化的时候运作得要更好。

添加验证

在我们关心 Address 对象的存储之前,我们应该确保用户所提供的值是合理的。例如,有些输入域是必填的,而phone number和email address则各有其特殊的格式。

BeanEditForm会在每个属性的输入域、getter方法或者setter方法上检查Tapestry特殊的注解,@Validate

对Address实体进行修改,更新一下lastName、firstName、street1、city、state和zip输入域,每个都加一个@Validate注解:

@Validate("required")

public String firstName;

那个字符串“required”是什么?它就是你指定的验证。它是一系列用来指定需要什么类型的验证的名称。Tapestry内置了许多验证器,注入“required”、“minLength”以“maxLength”。和其它地方一样,Tapestry对大小写不敏感。

你可以应用多个验证,只要将验证器的名称以逗号分隔就行了。某些验证器是可以被配置的(用一个等于符号)。这样你就能用“required,minLength=5”描述一个输入域必须被指定值,而且必须至少有5个字符长,这样的验证。

你会很容易感到迷惑,当你修改了实体类,比如添加了一个@Validate注解,可是并没有在浏览器中看到结果。只有component类,和(大多数)位于Tapestry service层的类是动态加载的。数据和实体对象并不会动态地重新加载。

重启应用程序,并刷新你的浏览器,然后点击Create Address 按钮:

就在点击Create Address的一瞬间:所有输入域都已经完成了验证并显示出错误提示。每个验证出问题的输入域都以红色高亮显示,并添加了错误消息。此外,每个验证出问题的输入域的label也是红色高亮的,以更加清晰的表明哪儿出错了。鼠标输入指针也已经被移动到第一个发现问题的输入域中。而所有这些都发生在客户端,没有跟应用程序的后台有任何通信。

所有的错误都一更正,表单就会提交,而验证也会在服务端被执行(以防客户端的JavaScript已经被禁用了)。

那么……再加更多一点有趣的验证,而不仅仅只是“required or not”,如何。Tapestry拥有对于基于输入域长度和对于几个输入域值的验证的验证支持,包括正则表达式。Zip code就能相当容易的被表示成正则表达式。

@Validate("required,regexp=^\\d{5}(-\\d{4})?$")

public String zip;

让我们来试试,重启应用程序并在zip code中输入“abc”看看:

这就是在你输入“abc”并点击Create Address按钮后所看到的。

现代浏览器在表单被提交时自动验证正则表达式,如上所示。老一点的浏览器并没有这种自动化的支持,不过仍然会验证输入框,在必填的输入域上使用跟之前的截图相同的样式装饰。

无论如何,这都是正确的验证行为,但反馈的消息是错误的。你的用户不会想要知道、也并不关心什么正则表达式。

幸运的是,自定义验证消息也很容易。我们所要做的一切就是知道属性的名称(“zip”)还有验证器的名称(“validator”)。然后我们就可以将其录入到CreateAddress的消息清单中:

zip-regexp-message=Zip Codes are five or nine digits.  Example: 02134 or 90125-1655.

刷新页面并再次提交:

这个小把戏不只是能用于正则表达式(regexp)验证器,对于任何验证器都有效。

让我们在更进一步。原来,我们还可以吧正则表达式一道消息清单中。如果你只是在@Validation注解中提供验证器的名称,Tapestry机会以限定的值,以及验证器消息,来对包含了page的消息清单进行搜索。针对正则表达式验证器的限定值就是对应的正则表达式。

@Validate("required,regexp")

public String zip;

现在,只要将正则表达式放到CreateAddress消息清单中就可以了:

zip-regexp=^\\d{5}(-\\d{4})?$

zip-regexp-message=Zip Codes are five or nine digits.  Example: 02134 or 90125-1655.

重启之后你就会看到……效果是一样的。不过当我们开始要创建更加复杂的正则表达式时,把它们放在消息清单中就比放到注解的值里面要好很多很多。而在消息清单里面,你就不需要因为修改或者调整了正则表达式而每次都得重启应用程序了。

这里我们还可以更进一步,为phone number和e-mail address加入更多的正则表达式。对于BeanEditForm component的进一步定制我们了解的还远不够。

现在你也许会对表单成功提交(没有验证错误)之后会发生生么感到好奇,这就是我们接下需要关心的事情了。

接下来是:在Tapestry中一起使用Hibernate

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏逢魔安全实验室

微软公式编辑器系列漏洞分析(一):CVE-2017-11882

? 0x00 简介 CVE-2017-11882为Office内存破坏漏洞。攻击者可以利用漏洞以当前登录的用户的身份执行任意命令。所影响的组件是Office...

3395
来自专栏Golang语言社区

Golang Template 简明笔记

作者:人世间 链接:https://www.jianshu.com/p/05671bab2357 來源:简书 前后端分离的Restful架构大行其道,传统的模板...

9176
来自专栏屈定‘s Blog

Java--死锁以及死锁的排查

清单一代码有点长,但是逻辑很简单,有两个临界区变量lockA,lockB,线程A先获取到lockA在获取lockB,线程B则与之相反顺序获取锁,那么就可能会有以...

5773
来自专栏Android 研究

Retrofit解析2之使用简介

前面介绍完RESTful之后,我们先来初步认识下Retrofit的使用"姿势"。本文的主要内容如下:

7593
来自专栏章鱼的慢慢技术路

一步步使用Code::Blocks进行设置断点调试程序

1853
来自专栏Android自学

ThinkPHP使用Smarty模板引擎的流程及注意事项

1303
来自专栏Aloys的开发之路

Linux快捷键

Shell 快捷键 <Ctrl k>:删除从光标到行尾的部分 <Ctrl u>:删除从光标到行首的部分 <Alt d>:删除从光标到当前...

2729
来自专栏Java开发者杂谈

线程间通信

  如果一个多线程程序中每个线程处理的资源没有交集,没有依赖关系那么这是一个完美的处理状态。你不用去考虑临界区域(critical section),不用担心存...

3819
来自专栏地方网络工作室的专栏

Python3 初学实践案例(2)将源目录中的图片用MD5命名并可以设定目标目录

Python3 初学实践案例(2)将源目录中的图片用MD5重命名后移动或复制到目标文件夹 尝试了一下用 python 实现了一个生成密码的程序。感觉还是比较好上...

25910
来自专栏奔跑的蛙牛技术博客

javaBean 简单理解JavaBean简单及使用

PO:persistant object持久对象,可以看成是与数据库中的表相映射的java对象。最简单的PO就是对应数据库中某个表中的一条记录,多个记录可以用P...

1594

扫码关注云+社区

领取腾讯云代金券