如何创建动态 JSF 表单域
Posted
技术标签:
【中文标题】如何创建动态 JSF 表单域【英文标题】:How to create dynamic JSF form fields 【发布时间】: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>
<my:someCustomComponent>
然后会返回适当的 JSF 表单组件(即标签、输入文本)
另一种方法是只显示<my:someCustomComponent>
,然后返回带有表单元素的htmlDataTable
。 (我认为这可能更容易做到)。
哪种方法最好?有人可以向我展示一些链接或代码,其中显示了我如何创建它吗?我更喜欢完整的代码示例,而不是“你需要javax.faces.component.UIComponent
的子类”之类的答案。
【问题讨论】:
【参考方案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
的 <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.xsl
将persons.xml
转换为persons.xhtml
,最后将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
键field1
、field2
、field3
等获得。
【讨论】:
嗨@BalusC。感谢您的广泛回答。但是,我不确定我是否可以从我们当前的模型中受益。是的,我们通过 XML 获取数据,但是它已经通过 Smooks 传输到 JavaBean (xml2Java)。所以我不确定我能按照你的建议做...... 是否必须将persons.xml
和persons.xsl
存储到此路径-.getRealPath("/")
?当我尝试将这些文件移动到 .getRealPath("/public_resources/xsl_xml")
(以及 <xsl:template match="/public_resources/xsl_xml/persons">
)时,它会抱怨,Content is not allowed in Prolog
- 生成的 XHTML 文件格式不再正确。
@Tiny <xsl:template match>
必须代表 XML 结构,而不是 XML 文件所在的路径。不要更改它。【参考方案2】:
由于来源实际上不是 XML,而是 Javabean,而另一个答案不值得编辑成完全不同的风格(它可能仍然对其他人的未来参考有用),我将根据 Javabean-origin 添加另一个答案。
当源是 Javabean 时,我基本上看到三个选项。
利用 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
后面的模型(如上面的 sn-p)至少在 JSF 视图范围内没有改变时,才能使用 JSTL。另请参阅 JSTL in JSF2 Facelets... makes sense? 相反,将 binding
用于 bean 属性仍然是“不好的做法”。
至于<ui:repeat><div>
,你使用哪个迭代组件并不重要,你甚至可以像在最初的问题中那样使用<h:dataTable>
,或者组件库特定的迭代组件,例如<p:dataGrid>
或@ 987654336@。 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
块。
在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 的属性保存时潜在的序列化问题和内存泄漏。
如果您仍在使用 <f:event>
不可用的 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?
使用自定义渲染器创建自定义组件。我不会发布完整的示例,因为那是很多代码,毕竟这将是一个非常紧耦合和特定于应用程序的混乱。
每个选项的优缺点应该很清楚。它从最容易和最好维护到最难和最难维护,然后从最不重用到最好重用。您可以选择最适合您的功能要求和当前情况的选项。
应该注意的是,绝对没有在Java中是only可能的(方式#2)而在XHTML+XML中是不可能的(方式#1)。在 XHTML+XML 中一切皆有可能与在 Java 中一样好。许多初学者在动态创建组件方面低估了 XHTML+XML(尤其是 <ui:repeat>
和 JSTL),并错误地认为 Java 将是“唯一的”方式,而这通常只会导致代码脆弱和混乱。
【讨论】:
还有第四种选择:PrimeFaces 扩展组件:DynaForm (primefaces.org/showcase-ext/views/home.jsf)。这有一些限制,但对于大多数用户来说已经足够了。 嗨 BalusC,我是你的忠实粉丝。我一直在通过您的回答学习,我需要您的邮件 ID 来讨论我现在面临的问题。请通过 qadir.hussain99@gmail.com 将您的 ID 邮寄给我以上是关于如何创建动态 JSF 表单域的主要内容,如果未能解决你的问题,请参考以下文章