我正在编写一个需要国际化的JSF应用程序。为此,我创建了MultilingualString:
public class MultilingualString {
/* The Language class is basically a wrapper for a java.util.Locale */
private Map<Language, String> strings;
/* business methods, getters, setters */
}
现在,有多个表单需要填充MultilingualString,并且每次需要将这样的对象放入表单时重复c:forEach循环是非常难看的。因此,我听说了JSF复合组件,并尝试为此编写一个。
这是我的inputMultilingualString.xhtml:
<ui:component xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:composite="http://xmlns.jcp.org/jsf/composite"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<composite:interface componentType="inputMultilingualString">
<composite:attribute name="value" required="true"
type="com.tob.entities.internationalization.MultilingualString"/>
<composite:attribute name="languages" type="java.util.List" default="#{null}"/>
</composite:interface>
<composite:implementation>
<f:event type="preRenderComponent" listener="#{cc.init}"/>
<h:dataTable id="#{cc.clientId}" value="#{cc.languages}" var="language">
<h:column>
<h:outputLabel value="#{language}"/>
</h:column>
<h:column>
<h:inputText binding="#{cc.inputs[language]}"/>
</h:column>
</h:dataTable>
</composite:implementation>
</ui:component>
所以我希望value属性是MultilingualString的一个实例,languages属性是List of Language的一个实例。如果languages属性为空,我希望复合组件在dataTable中为MultilingualString中包含的映射中的每个条目显示一行。
现在这是我的"支持组件"在InputMultilingualString.java:
@FacesComponent(value = "inputMultilingualString", createTag = true)
public class InputMultilingualString extends UIInput implements NamingContainer {
private final Map<Language, UIInput> inputs = new HashMap();
private List<Language> languages;
@Override
public String getFamily() {
return (UINamingContainer.COMPONENT_FAMILY);
}
public void init() {
List<Language> ls = (List<Language>) this.getAttributes().get("languages");
MultilingualString ms = (MultilingualString) this.getValue();
/* Setting languages */
if (ls != null) {
this.setLanguages(ls);
} else {
this.languages = new ArrayList();
this.languages.addAll(ms.getStrings().keySet());
}
/* Initializing inputs */
UIInput tmp;
for (Language l : this.languages) {
tmp = new UIInput();
tmp.setValue(ms.getString(l));//
this.inputs.put(l, tmp);
}
}
@Override
public String getSubmittedValue() {
String ret = new String();
for (Map.Entry<Language, UIInput> entry : this.inputs.entrySet()) {
if (entry.getValue() != null) {
if (!ret.isEmpty()) {
ret += ',';
}
ret += entry.getKey().getLanguageTag(); // NullPointerException here when the form is submitted
ret += "=" + entry.getValue().getSubmittedValue();
}
}
return (ret);
}
@Override
protected Object getConvertedValue(FacesContext context, Object submittedValue) {
MultilingualString ms = (MultilingualString) this.getValue();
String[] entries = ((String) submittedValue).split(",");
String[] pair;
Language language;
for (String entry : entries) {
pair = entry.split("=");
language = new Language();
language.setLanguageTag(pair[0]);
ms.addString(language, pair[1]);
}
return (ms);
}
public List<Language> getLanguages() {
return (this.languages);
}
public void setLanguages(List<Language> languages) {
this.languages = languages;
}
public Map<Language, UIInput> getInputs() {
return (this.inputs);
}
}
为了实现我想要在哪种语言上显示输入的规则,我向支持组件添加了一个languages属性,并在preRenderComponent事件中调用的init方法中对其进行了初始化。语言列表已正确初始化。
下面是我使用复合组件的方法:<ui:composition template="/Templates/Common.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:tob="http://xmlns.jcp.org/jsf/composite/components"
xmlns:p="http://primefaces.org/ui">
<ui:define name="content">
<h:form id="testForm">
<tob:inputMultilingualString value="#{testBean.ms}" languages="#{testBean.languages}"/>
<!-- The testBean.ms contains :
[English] => string-English
[français] => string-français
[русский] => string-русский
And the testBean.languages contains a list of Language objects for English, French, and Russian -->
<p:commandButton value="Submit" action="#{testBean.submit()}"/>
</h:form>
</ui:define>
</ui:composition>
问题是:
- 如果作为值输入的MultilingualString已经包含一些字符串,则它们不会显示在inputText中,因为如果您填充inputText值属性(我阅读了BalusC关于该主题的文章,他不需要填充值属性以使他的下拉列表具有正确的值)。我读到一个BalusC的答案在stackoverflow的某个地方,在inputText的绑定属性中引用的UIInput是创建的,如果它被评估为null,这就是为什么我试图在init方法中初始化它们,但到目前为止没有运气。
- 当我提交表单时,我在getSubmittedValue()方法的getKey()调用中获得NullPointerException。这怎么可能呢?
我希望这是清楚的,有人可以帮助我!谢谢!
编辑:我使用的是GlassFish 4,我手动更新了Mojarra到2.2.6
目前发布的代码中有两个技术问题:
-
你在一个变量上使用
binding
,这个变量只在视图渲染时可用。binding
属性在视图构建时运行,而不是在视图呈现时运行。在本例中,执行binding
时,#{language}
为null
。参见'binding'属性在JSF中工作吗?何时以及如何使用?此外,您似乎期望生成多个<h:inputText>
组件,但事实并非如此。只有一个在呈现视图期间被多次重用。只有当您使用<c:forEach>
而不是<h:dataTable>
时,才会生成多个物理上的<h:inputText>
组件。参见JSF2 Facelets中的JSTL…有道理吗? -
您没有为回发保存组件的状态。您应该删除
languages
属性,并让getter和setter委托给getStateHelper()
。参见扩展UIComponentBase时如何保存状态
然而,整体方法是笨拙的。您不需要一个支持组件来满足功能需求。只需在MultilingualString
上添加List<Languages>
getter,并将其直接用作languages
属性的default
。
所以,如果你把这个添加到MultilingualString
:
public List<Language> getLanguages() {
return new ArrayList<>(strings.keySet());
}
然后通过#{cc.attrs}
:
<cc:interface>
<cc:attribute name="value" required="true" type="com.tob.entities.internationalization.MultilingualString"/>
<cc:attribute name="languages" type="java.util.List" default="#{cc.attrs.value.languages}" />
</cc:interface>
<cc:implementation>
<h:dataTable value="#{cc.attrs.languages}" var="language">
<h:column>
<h:outputLabel value="#{language}"/>
</h:column>
<h:column>
<h:inputText value="#{cc.attrs.value.strings[language]}" />
</h:column>
</h:dataTable>
</cc:implementation>
那么它应该按预期工作。请注意,可以使用大括号符号[]
来引用动态映射键。这可能是你的解决方案的全部关键(你似乎没有意识到这一点,因此朝着一个过于复杂的解决方案努力)。