Chapter 9. Seam中的JSF表单验证

在普通JSF中,验证在视图中定义:

<h:form>
    <h:messages/>

    <div>
        Country:
        <h:inputText value="#{location.country}" required="true">
            <my:validateCountry/>
        </h:inputText>
    </div>

    <div>
        Zip code:
        <h:inputText value="#{location.zip}" required="true">
            <my:validateZip/>
        </h:inputText>
    </div>

    <h:commandButton/>
</h:form>

在实践中,这种方式常常违背了DRY原则,因为很多“validation”实际上依赖的约束是数据模型的一部分,而且也有很多方法引入数据库Schema定义。 Seam使用Hibernate Validator来提供对基于model的约束支持。

让我们从定义 Location 类的约束开始:

public class Location {
    private String country;
    private String zip;

    @NotNull
    @Length(max=30)
    public String getCountry() { return country; }
    public void setCountry(String c) { country = c; }

    @NotNull
    @Length(max=6)
    @Pattern("^\d*$")
    public String getZip() { return zip; }
    public void setZip(String z) { zip = z; }
}

好,这是一个不错的切入点,但在实践中使用自定义的约束可能比 Hibernate Validator 更优雅:

public class Location {
    private String country;
    private String zip;

    @NotNull
    @Country
    public String getCountry() { return country; }
    public void setCountry(String c) { country = c; }

    @NotNull
    @ZipCode
    public String getZip() { return zip; }
    public void setZip(String z) { zip = z; }
}

无论我们使用哪种方式,都不需要在JSF页面中指定验证类型。我们可以使用 <s:validate> 来验证定义在model对象上的约束。

<h:form>
    <h:messages/>

    <div>
        Country:
        <h:inputText value="#{location.country}" required="true">
            <s:validate/>
        </h:inputText>
    </div>

    <div>
        Zip code:
        <h:inputText value="#{location.zip}" required="true">
            <s:validate/>
        </h:inputText>
    </div>

    <h:commandButton/>

</h:form>

注意: 在model上指定 @NotNull 并不能 在控制上省去 required="true"! 这是因为JSF验证架构的限制。

这种方式在model中 定义 约束,然后在表现层中 展示 约束违例 — 这显然是一种更好的设计。

但是,这并不比我们之前的方法简便多少,所以让我们试试 <s:validateAll>

<h:form>

    <h:messages/>

    <s:validateAll>

        <div>
            Country:
            <h:inputText value="#{location.country}" required="true"/>
        </div>

        <div>
            Zip code:
            <h:inputText value="#{location.zip}" required="true"/>
        </div>

        <h:commandButton/>

    </s:validateAll>

</h:form>

这个标签只是简单地给表单中的每个输入框增加 <s:validate> 标签。 对于一个大的表单来说,这能够减少很多打字工作量!

现在我们需要做些事情来显示验证失败时的反馈消息。当前我们是在表单的上方显示所有消息。 而我们真正想要做的是在值域后面显示错误的提示消息(普通JSF也可以实现),高亮显示字段和标签(普通JSF无法实现), 更好的是在字段后面显示一些图片(普通JSF同样无法实现)。 我们还希望在每个必须输入的字段所对应的标记前显示一个有颜色的*号。

这的确给表单中的每个字段增加了不少功能。 我们不希望给表单中的每一个字段指定图片、消息和输入字段的高亮显示和布局。所以我们将通用布局定义在facelets模板中。

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                xmlns:h="http://java.sun.com/jsf/html"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:s="http://jboss.com/products/seam/taglib">

    <div>

        <s:label styleClass="#{invalid?'error':''}">
            <ui:insert name="label"/>
            <s:span styleClass="required" rendered="#{required}">*</s:span>
        </s:label>

        <span class="#{invalid?'error':''}">
            <h:graphicImage src="img/error.gif" rendered="#{invalid}"/>
            <s:validateAll>
                <ui:insert/>
            </s:validateAll>
        </span>

        <s:message styleClass="error"/>

    </div>

</ui:composition>

我们可以通过 <s:decorate> 让每一个表单字段都使用这个模板。

<h:form>

    <h:messages globalOnly="true"/>

    <s:decorate template="edit.xhtml">
        <ui:define name="label">Country:</ui:define>
        <h:inputText value="#{location.country}" required="true"/>
    </s:decorate>

    <s:decorate template="edit.xhtml">
        <ui:define name="label">Zip code:</ui:define>
        <h:inputText value="#{location.zip}" required="true"/>
    </s:decorate>

    <h:commandButton/>

</h:form>

最后,我们可以使用 RichFaces Ajax 来在用户浏览表单时显示验证消息:

<h:form>

    <h:messages globalOnly="true"/>

    <s:decorate id="countryDecoration" template="edit.xhtml">
        <ui:define name="label">Country:</ui:define>
        <h:inputText value="#{location.country}" required="true">
            <a:support event="onblur" reRender="countryDecoration" bypassUpdates="true"/>
        </h:inputText>
    </s:decorate>

    <s:decorate id="zipDecoration" template="edit.xhtml">
        <ui:define name="label">Zip code:</ui:define>
        <h:inputText value="#{location.zip}" required="true">
            <a:support event="onblur" reRender="zipDecoration" bypassUpdates="true"/>
        </h:inputText>
    </s:decorate>

    <h:commandButton/>

</h:form>

最好为页面上的重要控件定义显式的id,特别是当你希望用像 Selenium 这样的工具来进行UI自动化测试时。 如果你没有提供显式的id,JSF会生成它们,但任何页面上的改动都会导致生成的值发生变化。

<h:form id="form">

    <h:messages globalOnly="true"/>

    <s:decorate id="countryDecoration" template="edit.xhtml">
        <ui:define name="label">Country:</ui:define>
        <h:inputText id="country" value="#{location.country}" required="true">
            <a:support event="onblur" reRender="countryDecoration" bypassUpdates="true"/>
        </h:inputText>
    </s:decorate>

    <s:decorate id="zipDecoration" template="edit.xhtml">
        <ui:define name="label">Zip code:</ui:define>
        <h:inputText id="zip" value="#{location.zip}" required="true">
            <a:support event="onblur" reRender="zipDecoration" bypassUpdates="true"/>
        </h:inputText>
    </s:decorate>

    <h:commandButton/>

</h:form>

但是,如果你想在验证失败时指定去显示不同的消息又该怎么做呢?可以使用Seam对Hibernate Validator的消息绑定: (同样也包括其中那些类似el表达式和独立视图消息绑定之类的各种好处)

public class Location {
    private String name;
    private String zip;

    // Getters and setters for name

    @NotNull
    @Length(max=6)
    @ZipCode(message="#{messages['location.zipCode.invalid']}")
    public String getZip() { return zip; }
    public void setZip(String z) { zip = z; }
}
location.zipCode.invalid = The zip code is not valid for #{location.name}