【问题标题】:How to create dynamic JSF form fields如何创建动态 JSF 表单域
【发布时间】: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 的子类”之类的答案。

【问题讨论】:

    标签: jsf components facelets dynamic-forms


    【解决方案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 时,我基本上看到三个选项。

    1. 利用 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 块。


    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 重复”问题(这样您就可以安全地使用例如视图范围的 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?


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


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

    应该注意的是,绝对没有在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 邮寄给我
    猜你喜欢
    • 1970-01-01
    • 2021-07-27
    • 2023-03-21
    • 2011-08-14
    相关资源
    最近更新 更多