如何创建动态JSF表单字段

25

我发现了一些类似这个问题的问题,但是有这么多方法可以完成,它让我变得更加困惑。

我们正在获取一个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>
<my:someCustomComponent>将返回适当的JSF表单组件(如标签、inputText)。
另一种方法是仅显示<my:someCustomComponent>,然后它将返回一个具有表单元素的HtmlDataTable。(我认为这可能更容易做到)。
哪种方法最好?有人可以向我展示一些链接或代码,展示如何创建这个?我喜欢完整的代码例子,而不是像“您需要javax.faces.component.UIComponent的子类”这样的答案。
2个回答

56

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


当原始内容为Javabean时,我基本上看到三个选项:

  1. 利用JSF的rendered属性或甚至是JSTL的<c:choose>/<c:if>标签来有条件地渲染或构建所需的组件。以下是使用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>
    

    如何制作JSF组合组件的网格?中可以找到JSTL方法的示例。不,JSTL绝对不是“不良做法”。这个神话是JSF 1.x时代的剩余物,因为初学者没有清楚地理解JSTL的生命周期和功能而持续了太长时间。简而言之,在上面的片段中作为#{bean.fields}模型背后从不发生变化时,您只能使用JSTL至少在JSF视图范围内。另请参见JSF2 Facelets中的JSTL…有意义吗?相反,绑定到Bean属性仍然是“不良做法”。

    至于<ui:repeat><div>,使用哪种迭代组件并不重要,甚至可以像最初的问题中一样使用<h:dataTable>,或者使用特定于组件库的迭代组件,例如<p:dataGrid><p:dataList>如果需要,将大块代码重构为包含或标记文件

    至于收集提交的值,#{bean.values}应指向已经预创建的Map<String, Object>。一个HashMap就足够了。对于可以设置多个值的控件,您可能需要预填充地图。然后应以List<Object>作为值来预填充它。请注意,我期望Field#getType()是一个enum,因为这样可以在Java代码侧轻松处理。然后可以使用switch语句代替难看的if/else块。


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

  3. <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...
            }
        }
    }
    
    < p > (注:不要自己创建HtmlForm!使用由JSF创建的,这个不会是null

    这保证了树在恰当的时刻被填充,并使得getter免于业务逻辑,避免了潜在的“重复组件ID”问题,当#{bean}比请求范围更广时(所以你可以安全地在这里使用一个视图作用域bean),并且使bean免于UIComponent属性,从而避免了潜在的序列化问题和记忆泄露,当组件作为可序列化bean的属性时。

    如果你仍在使用JSF 1.x,其中<f:event>不可用,请使用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?


    创建具有自定义渲染器的自定义组件。我不会发布完整示例,因为那将是非常紧密耦合和应用程序特定的混乱代码。


    每个选项的优缺点应该清晰明了。从最容易和最可维护到最难和最不可维护,因此也从最少可重用到最可重用。选择最适合您的功能要求和当前情况的选项由您决定。

    应该注意的是,在XHTML+XML(第1种方法)中没有任何只有Java(第2种方法)才能实现而在XHTML+XML中不可能的东西。在XHTML+XML中,动态创建组件(特别是<ui:repeat>和JSTL)的能力往往被新手低估,并错误地认为Java是“唯一”的方式,但这通常最终只会导致脆弱和混乱的代码。


1
有第四种选择:PrimeFaces扩展组件:DynaForm(http://www.primefaces.org/showcase-ext/views/home.jsf)。这有一些限制,但对于大多数用户来说已经足够了。 - Dimitri Dewaele
嗨,BalusC,我是你的忠实粉丝。我一直在通过你的答案学习,并且我需要你的邮件地址来讨论我现在面临的问题。请发送邮件到qadir.hussain99@gmail.com告诉我你的邮箱地址。 - Qadir Hussain

18
如果数据源是XML,我建议采用完全不同的方法:XSL。Facelets基于XHTML。您可以轻松使用XSL从XML转换为XHTML。这可以通过一个相当不错的Filter实现,在JSF开始工作之前启动该过滤器即可。
以下是一个示例: 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<servlet-name>,并假定FacesServlet本身被映射到*.jsf<url-pattern>上。

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 有一定的学习曲线,但我认为它是正确的工具,因为源是 XML,目的地也是基于 XML 的。

要在表单和托管 bean 之间进行映射,只需使用一个 Map<String, Object>。如果你将输入字段命名为下面这样:

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

提交的值将通过 field1field2field3 等键作为 Map 的可用值。


嗨@BalusC。感谢您的详细回答。但是,我不确定在我们当前的模型下是否可以从中受益。 是的,我们通过XML获取数据,但它已经通过Smooks转换为JavaBean(xml2Java)。 所以我不确定我能做到你在这里建议的事情... - Shervin Asgari
在存储persons.xmlpersons.xsl时是否必须将其存储到此路径 - .getRealPath("/")?当我尝试将这些文件移动到.getRealPath("/public_resources/xsl_xml")(连同<xsl:template match="/public_resources/xsl_xml/persons">)时,它会抱怨Content is not allowed in Prolog - 生成的XHTML文件不再格式良好。 - Tiny
1
@Tiny <xsl:template match> 必须表示 XML 结构,而不是 XML 文件所在的路径。请勿更改它。 - BalusC

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接