如何创建动态JSF表单字段

时间:2010-08-18 09:25:28

标签: jsf components facelets dynamic-forms

我发现了类似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的子类”这样的答案。

2 个答案:

答案 0 :(得分:54)

由于起源实际上不是XML,而是Javabean,而另一个答案不值得编辑成完全不同的风格(对于其他人未来的引用可能仍然有用),我将根据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>
    

    JSTL方法的一个例子可以在How to make a grid of JSF composite component?找到。不,JSTL绝对不是一个“坏习惯”。这个神话是JSF 1.x时代的遗留问题,并且持续时间太长,因为初学者并没有清楚地理解JSTL的生命周期和权力。到目前为止,只有在上述代码段中#{bean.fields}后面的模型在至少JSF视图范围内不会发生变化时,才能使用JSTL。另请参阅JSTL in JSF2 Facelets... makes sense?相反,将binding用于bean属性仍然是一种“不良做法”。

    对于<ui:repeat><div>,使用哪个迭代组件并不重要,您甚至可以在初始问题中使用<h:dataTable>,或者使用组件库特定的迭代组件,例如{ {1}}或<p:dataGrid>Refactor if necessary the big chunk of code to an include or tagfile

    关于收集提交的值,<p:dataList>应指向已预先创建的#{bean.values}Map<String, Object>就足够了。如果控件可以设置多个值,您可能需要预先填充地图。然后,您应该使用HashMap作为值进行预填充。请注意,我希望List<Object>Field#getType(),因为这样可以简化Java代码方面的处理。然后,您可以使用enum语句而不是讨厌的switch块。


  2. 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... } } } !使用JSF创建的一个,这个永远不会HtmlForm

    这保证了树在恰当的时刻填充,并保持getter没有业务逻辑,并且当null在比请求范围更广的范围内时避免潜在的“重复组件ID”麻烦(所以你可以安全地使用例如这里的视图范围bean),并保持bean没有#{bean}属性,这反过来避免了当组件被保存为可序列化bean的属性时潜在的序列化问题和内存泄漏。

    如果您仍然使用UIComponent不可用的JSF 1.x,而是通过<f:event>

    将表单组件绑定到请求(而不是session!)作用域bean
    binding

    然后在表格的吸气器中懒洋洋地填充它:

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

    使用public HtmlForm getForm() { if (form == null) { form = new HtmlForm(); // ... (continue with code as above) } return form; } 时,了解UI组件基本上是请求范围非常重要,并且绝对不应该在更广泛的范围内将其指定为bean的属性。另请参阅How does the 'binding' attribute work in JSF? When and how should it be used?


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


  4. 每个选项的利弊都应该清楚。它从最简单和最好的可维护到最硬和最不可维护,从而也从最不可重复使用到最佳可重复使用。您可以根据自己的功能需求和当前情况选择最适合的选项。

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

答案 1 :(得分: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>的{​​{1}},并假设FacesServlet本身映射到FacesServlet <url-pattern>

*.jsf

http://example.com/context/persons.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); } persons.xml转换为persons.xhtml,最后将persons.xsl放在JSF预期的位置。

是的,XSL有一点学习曲线,但IMO是适合工作的正确工具,因为源是XML而目标是基于XML的。

要在表单和托管bean之间进行映射,只需使用persons.xhtml。如果您将输入字段命名为

Map<String, Object>

提交的值将由<h:inputText value="#{bean.map.field1}" /> <h:inputText value="#{bean.map.field2}" /> <h:inputText value="#{bean.map.field3}" /> ... 个键Mapfield1field2等提供。