如何创建动态 JSF 表单域

Posted

技术标签:

【中文标题】如何创建动态 JSF 表单域【英文标题】:How to create dynamic JSF form fields 【发布时间】:2011-03-31 10:48:59 【问题描述】:

我发现了一些类似的问题,例如this one,但是有很多方法可以做到这一点,这让我更加困惑。

我们正在读取一个XML 文件。这个XML 包含一些需要显示的表单域的信息。

所以我创建了这个自定义 DynamicField.java,它包含我们需要的所有信息:

public class DynamicField 
  private String label; // label of the field
  private String fieldKey; // some key to identify the field
  private String fieldValue; // the value of field
  private String type; // can be input,radio,selectbox etc

  // Getters + setters.

所以我们有一个List<DynamicField>

我想遍历这个列表并填充表单字段,使其看起来像这样:

<h:dataTable value="#dynamicFields" var="field">
    <my:someCustomComponent value="#field" />
</h:dataTable>

&lt;my:someCustomComponent&gt; 然后会返回适当的 JSF 表单组件(即标签、输入文本)

另一种方法是只显示&lt;my:someCustomComponent&gt;,然后返回带有表单元素的htmlDataTable。 (我认为这可能更容易做到)。

哪种方法最好?有人可以向我展示一些链接或代码,其中显示了我如何创建它吗?我更喜欢完整的代码示例,而不是“你需要javax.faces.component.UIComponent 的子类”之类的答案。

【问题讨论】:

【参考方案1】:

如果源是 XML,我建议采用完全不同的方法:XSL。 Facelets 是基于 XHTML 的。您可以轻松地使用 XSL 从 XML 转到 XHTML。这可以通过在 JSF 开始工作之前启动的有点体面的 Filter 来实现。

这是一个启动示例。

persons.xml

<?xml version="1.0" encoding="UTF-8"?>
<persons>
    <person>
        <name>one</name>
        <age>1</age>
    </person>
    <person>
        <name>two</name>
        <age>2</age>
    </person>
    <person>
        <name>three</name>
        <age>3</age>
    </person>
</persons>

persons.xsl

<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html">

    <xsl:output method="xml"
        doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN"
        doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/>

    <xsl:template match="persons">
        <html>
        <f:view>
            <head><title>Persons</title></head>
            <body>
                <h:panelGrid columns="2">
                    <xsl:for-each select="person">
                        <xsl:variable name="name"><xsl:value-of select="name" /></xsl:variable>
                        <xsl:variable name="age"><xsl:value-of select="age" /></xsl:variable>
                        <h:outputText value="$name" />
                        <h:outputText value="$age" />
                    </xsl:for-each>
                </h:panelGrid>
            </body>
        </f:view>
        </html>
    </xsl:template>
</xsl:stylesheet>

JsfXmlFilter 映射到 FacesServlet&lt;servlet-name&gt; 并假设 FacesServlet 本身映射到 *.jsf&lt;url-pattern&gt;

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException

    HttpServletRequest r = (HttpServletRequest) request;
    String rootPath = r.getSession().getServletContext().getRealPath("/");
    String uri = r.getRequestURI();
    String xhtmlFileName = uri.substring(uri.lastIndexOf("/")).replaceAll("jsf$", "xhtml"); // Change this if FacesServlet is not mapped on `*.jsf`.
    File xhtmlFile = new File(rootPath, xhtmlFileName);

    if (!xhtmlFile.exists())  // Do your caching job.
        String xmlFileName = xhtmlFileName.replaceAll("xhtml$", "xml");
        String xslFileName = xhtmlFileName.replaceAll("xhtml$", "xsl");
        File xmlFile = new File(rootPath, xmlFileName);
        File xslFile = new File(rootPath, xslFileName);
        Source xmlSource = new StreamSource(xmlFile);
        Source xslSource = new StreamSource(xslFile);
        Result xhtmlResult = new StreamResult(xhtmlFile);

        try 
            Transformer transformer = TransformerFactory.newInstance().newTransformer(xslSource);
            transformer.transform(xmlSource, xhtmlResult);
         catch (TransformerException e) 
            throw new RuntimeException("Transforming failed.", e);
        
    

    chain.doFilter(request, response);

由http://example.com/context/persons.jsf 运行,此过滤器将启动并使用persons.xslpersons.xml 转换为persons.xhtml,最后将persons.xhtml 放在JSF 期望的位置。

的确,XSL 有一些学习曲线,但它是 IMO 适合这项工作的工具,因为源是 XML,目标也是基于 XML。

要在表单和托管 bean 之间进行映射,只需使用 Map&lt;String, Object&gt;。如果你这样命名输入字段

<h:inputText value="#bean.map.field1" />
<h:inputText value="#bean.map.field2" />
<h:inputText value="#bean.map.field3" />
...

提交的值将通过Mapfield1field2field3等获得。

【讨论】:

嗨@BalusC。感谢您的广泛回答。但是,我不确定我是否可以从我们当前的模型中受益。是的,我们通过 XML 获取数据,但是它已经通过 Smooks 传输到 JavaBean (xml2Java)。所以我不确定我能按照你的建议做...... 是否必须将persons.xmlpersons.xsl 存储到此路径-.getRealPath("/")?当我尝试将这些文件移动到 .getRealPath("/public_resources/xsl_xml")(以及 &lt;xsl:template match="/public_resources/xsl_xml/persons"&gt;)时,它会抱怨,Content is not allowed in Prolog - 生成的 XHTML 文件格式不再正确。 @Tiny &lt;xsl:template match&gt; 必须代表 XML 结构,而不是 XML 文件所在的路径。不要更改它。【参考方案2】:

由于来源实际上不是 XML,而是 Javabean,而另一个答案不值得编辑成完全不同的风格(它可能仍然对其他人的未来参考有用),我将根据 Javabean-origin 添加另一个答案。


当源是 Javabean 时,我基本上看到三个选项。

    利用 JSF rendered 属性甚至 JSTL &lt;c:choose&gt;/&lt;c:if&gt; 标签来有条件地渲染或构建所需的组件。下面是一个使用rendered 属性的例子:

    <ui:repeat value="#bean.fields" var="field">
        <div class="field">
            <h:inputText value="#bean.values[field.name]" rendered="#field.type == 'TEXT'" />
            <h:inputSecret value="#bean.values[field.name]" rendered="#field.type == 'SECRET'" />
            <h:inputTextarea value="#bean.values[field.name]" rendered="#field.type == 'TEXTAREA'" />
            <h:selectOneRadio value="#bean.values[field.name]" rendered="#field.type == 'RADIO'">
                <f:selectItems value="#field.options" />
            </h:selectOneRadio>
            <h:selectOneMenu value="#bean.values[field.name]" rendered="#field.type == 'SELECTONE'">
                <f:selectItems value="#field.options" />
            </h:selectOneMenu>
            <h:selectManyMenu value="#bean.values[field.name]" rendered="#field.type == 'SELECTMANY'">
                <f:selectItems value="#field.options" />
            </h:selectManyMenu>
            <h:selectBooleanCheckbox value="#bean.values[field.name]" rendered="#field.type == 'CHECKONE'" />
            <h:selectManyCheckbox value="#bean.values[field.name]" rendered="#field.type == 'CHECKMANY'">
                <f:selectItems value="#field.options" />
            </h:selectManyCheckbox>
        </div>
    </ui:repeat>
    

    JSTL 方法的示例可以在How to make a grid of JSF composite component? 找到,不,JSTL 绝对不是“坏习惯”。这个神话是 JSF 1.x 时代遗留下来的,并且持续了太久,因为初学者没有清楚地了解 JSTL 的生命周期和功能。就这一点而言,只有当#bean.fields 后面的模型(如上面的 sn-p)至少在 JSF 视图范围内没有改变时,才能使用 JSTL。另请参阅 JSTL in JSF2 Facelets... makes sense? 相反,将 binding 用于 bean 属性仍然是“不好的做法”。

    至于&lt;ui:repeat&gt;&lt;div&gt;,你使用哪个迭代组件并不重要,你甚至可以像在最初的问题中那样使用&lt;h:dataTable&gt;,或者组件库特定的迭代组件,例如&lt;p:dataGrid&gt;或@ 987654336@。 Refactor if necessary the big chunk of code to an include or tagfile.

    关于收集提交的值,#bean.values 应该指向一个已经预先创建的Map&lt;String, Object&gt;HashMap 就足够了。如果控件可以设置多个值,您可能需要预填充地图。然后,您应该使用 List&lt;Object&gt; 作为值预填充它。请注意,我希望Field#getType()enum,因为这样可以简化Java 代码端的处理。然后,您可以使用 switch 语句而不是讨厌的 if/else 块。


    postAddToView 事件监听器中以编程方式创建组件:

    <h:form id="form">
        <f:event type="postAddToView" listener="#bean.populateForm" />
    </h:form>
    

    与:

    public void populateForm(ComponentSystemEvent event) 
        HtmlForm form = (HtmlForm) event.getComponent();
        for (Field field : fields) 
            switch (field.getType())  // It's easiest if it's an enum.
                case TEXT:
                    UIInput input = new HtmlInputText();
                    input.setId(field.getName()); // Must be unique!
                    input.setValueExpression("value", createValueExpression("#bean.values['" + field.getName() + "']", String.class));
                    form.getChildren().add(input);
                    break;
                case SECRET:
                    UIInput input = new HtmlInputSecret();
                    // etc...
            
        
    
    

    (注意:不要自己创建HtmlForm!使用JSF创建的,这个永远不是null

    这保证了树在正确的时刻被填充,并使 getter 不受业务逻辑的影响,并避免#bean 的范围比请求范围更广时潜在的“组件 ID 重复”问题(这样您就可以安全地使用例如视图范围的 bean),并保持 bean 没有UIComponent 属性,这反过来又避免了当组件作为可序列化 bean 的属性保存时潜在的序列化问题和内存泄漏。

    如果您仍在使用 &lt;f:event&gt; 不可用的 JSF 1.x,请改为通过 binding 将表单组件绑定到请求(不是会话!)范围 bean

    <h:form id="form" binding="#bean.form" />
    

    然后在表单的getter中懒洋洋地填充:

    public HtmlForm getForm() 
        if (form == null) 
            form = new HtmlForm();
            // ... (continue with code as above)
        
        return form;
    
    

    在使用binding 时,了解 UI 组件基本上是请求范围的,绝对不应该被分配为更广泛范围内的 bean 的属性,这一点非常重要。另见How does the 'binding' attribute work in JSF? When and how should it be used?


    使用自定义渲染器创建自定义组件。我不会发布完整的示例,因为那是很多代码,毕竟这将是一个非常紧耦合和特定于应用程序的混乱。


每个选项的优缺点应该很清楚。它从最容易和最好维护到最难和最难维护,然后从最不重用到最好重用。您可以选择最适合您的功能要求和当前情况的选项。

应该注意的是,绝对没有在Java中是only可能的(方式#2)而在XHTML+XML中是不可能的(方式#1)。在 XHTML+XML 中一切皆有可能与在 Java 中一样好。许多初学者在动态创建组件方面低估了 XHTML+XML(尤其是 &lt;ui:repeat&gt; 和 JSTL),并错误地认为 Java 将是“唯一的”方式,而这通常只会导致代码脆弱和混乱。

【讨论】:

还有第四种选择:PrimeFaces 扩展组件:DynaForm (primefaces.org/showcase-ext/views/home.jsf)。这有一些限制,但对于大多数用户来说已经足够了。 嗨 BalusC,我是你的忠实粉丝。我一直在通过您的回答学习,我需要您的邮件 ID 来讨论我现在面临的问题。请通过 qadir.hussain99@gmail.com 将您的 ID 邮寄给我

以上是关于如何创建动态 JSF 表单域的主要内容,如果未能解决你的问题,请参考以下文章

JSF 2.0将动态表单保存到数据库[重复]

如何使用 Spring Security 3.1.3 和 JSF 创建一个 Bean 来验证我的登录表单

Odoo v9 - 编辑表单时设置动态域(不创建)

如何使用多个 JSF 表单

如何在 JSF 2.0 中使用表单重新呈现页面的一部分?

elementfrom表单如何限制表单域