如何创建动态JSF表单字段

问题描述 投票:20回答:2

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

我们正在获取正在阅读的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表单组件(即label,inputText)

另一种方法是只显示<my:someCustomComponent>,然后返回带有表单元素的HtmlDataTable。 (我认为这可能更容易)。

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

jsf components facelets dynamic-forms
2个回答
54
投票

由于原点实际上不是XML,而是Javabean,而另一个答案不值得编辑成完全不同的风格(它可能对其他人未来的引用仍然有用),我将添加另一个基于a的答案JavaBean的由来。


当原点是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> 可以在How to make a grid of JSF composite component?找到JSTL方法的一个例子。不,JSTL绝对不是一个“坏习惯”。这个神话是JSF 1.x时代的遗留问题,并且持续时间太长,因为初学者并没有清楚地理解JSTL的生命周期和权力。到目前为止,只有当#{bean.fields}背后的模型在上面的代码片段中至少在JSF视图范围内没有改变时,才可以使用JSTL。另请参阅JSTL in JSF2 Facelets... makes sense?相反,将binding用于bean属性仍然是一种“不好的做法”。 至于<ui:repeat><div>,使用哪个迭代组件并不重要,你甚至可以在初始问题中使用<h:dataTable>,或者使用组件库特定的迭代组件,例如<p:dataGrid><p:dataList>Refactor if necessary the big chunk of code to an include or tagfile。 至于收集提交的值,#{bean.values}应指向已经预先创建的Map<String, Object>。一个HashMap就足够了。如果控件可以设置多个值,您可能需要预先填充地图。然后,您应该使用List<Object>作为值进行预填充。请注意,我希望Field#getType()是一个enum,因为这样可以简化Java代码方面的处理。然后你可以使用switch语句而不是令人讨厌的if/else块。
  2. 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”问题(因此您可以安全地使用例如视图这里的scoped bean),并保持bean没有UIComponent属性,这反过来避免了当组件被保存为可序列化bean的属性时潜在的序列化问题和内存泄漏。 如果你仍然在JSF 1.x上<f:event>不可用,而是通过binding将表单组件绑定到请求(而不是session!)作用域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?
  3. 使用自定义渲染器创建自定义组件。我不会发布完整的例子,因为这是很多代码,毕竟这是一个非常紧密耦合和特定于应用程序的混乱。

每个选项的利弊都应该清楚。它从最简单和最好的可维护到最硬和最不可维护,从而也从最不可重复使用到最佳可重复使用。您可以选择最适合您的功能要求和当前情况的任何东西。

值得注意的是,绝对没有任何东西只能在Java(方式#2)中实现,而在XHTML + XML(方式#1)中是不可能的。 XHTML + XML中的一切都可以和Java一样好。许多初学者在动态创建组件时低估了XHTML + XML(特别是<ui:repeat>和JSTL),并错误地认为Java将是“唯一的”方式,而这通常只会导致脆弱和混乱的代码。


16
投票

如果原点是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映射在<servlet-name>FacesServlet上,并假设FacesServlet本身映射在<url-pattern>*.jsf上。

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.xmlpersons.xhtml转换为persons.xsl,最后将persons.xhtml放在JSF预期的位置。

确实,XSL有一些学习曲线,但它是IMO正确的工具,因为源是XML而目标是基于XML的。

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

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

提交的值将由Map keys field1field2field3等提供。

© www.soinside.com 2019 - 2024. All rights reserved.