如何创builddynamic的JSF表单域

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

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

所以我创build了这个定制的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 。 (我认为这可能更容易做到)。

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

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


基本上有三个选项,当起源是一个Javabean。

  1. 使用JSF rendered属性,甚至使用JSTL <c:choose> / <c:if>标记来有条件地呈现或构build所需的组件。 以下是使用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方法的一个例子可以在如何创build一个JSF复合组件的网格中find。 不,JSTL绝对不是一个“坏习惯”。 这个神话是JSF 1.x时代的遗留物,并且持续时间太长,因为初学者不清楚JSTL的生命周期和能力。 至此,只有当上面代码片段中的#{bean.fields}背后的模型至less在JSF视图范围内没有变化时,才能使用JSTL。 另请参阅JSF2 Facelets中的JSTL …有意义吗? 相反,使用binding到一个bean属性仍然是一个“坏习惯”。

    对于<ui:repeat><div> ,使用哪个迭代组件真的没有关系,甚至可以像在最初的问题中一样使用<h:dataTable> ,也可以使用组件库特定的迭代组件,例如<p:dataGrid><p:dataList> 。 如有必要,将大块代码重构为包含或标记文件 。

    至于收集提交的值, #{bean.values}应该指向一个已经预先创build的Map<String, Object> 。 一个HashMap就足够了。 在可以设置多个值的控件的情况下,您可能需要预先填充地图。 然后,您应该使用List<Object>作为值预填充它。 请注意,我期望Field#getType()是一个enum因为它简化了Java代码的处理。 然后可以使用switch语句而不是讨厌的if/else块。


  2. 以编程方式在postAddToView事件侦听器中创build组件:

     <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... } } } 

    (注意:不要自己创buildHtmlForm !使用JSF创build的,这个不会是null

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

    如果您仍然使用<f:event>不可用的JSF 1.x,则通过绑定将表单组件绑定到请求(不是会话!)作用域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的属性是非常重要的。 另请参见JSF中的“绑定”属性如何工作? 何时以及如何使用?


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


每个选项的利弊应该清楚。 它从最容易和最好维护到最困难,最不可维护,随后也从最less重用到最好重用。 你可以select最适合你的function要求和现状的任何东西。

值得注意的是,绝对没有任何东西 只能在Java(方式2)中使用,在XHTML + XML(方式1)中是不可能的。 在XHTML + XML中,一切都是可能的,就像在Java中一样。 许多初学者低估了dynamic创build组件的XHTML + XML(尤其是<ui:repeat>和JSTL),错误地认为Java将是“唯一的”方式,而这通常只会以脆弱和混乱的代码结束。

如果起源是XML,我build议去一个完全不同的方法: 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> 

映射到FacesServlet <servlet-name>上的JsfXmlFilter ,并假定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运行,这个filter将会启动并使用;persons.xslpersons.xhtml转换为persons.xsl ,最后在那里添加persons.xhtml ,在那里JSF期待它。

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

要执行表单和托pipebean之间的映射,只需使用Map<String, Object> 。 如果你像这样命名input字段

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

提交的值将由Map键字段1,字段2,字段3等提供。