問題描述
假設(shè)我想在我的應(yīng)用程序中導(dǎo)航,并動(dòng)態(tài)包含不同的 facelet 頁面.我有一個(gè)這樣的 commandLink:
Suppose I want to navigate in my application, and include different facelet pages dynamically. I have a commandLink like this:
<h:commandLink value="Link" action="#{navigation.goTo('someTest')}">
<f:ajax render=":content" />
</h:commandLink>
這就是我包含 facelet 的地方:
And this is where I include the facelet:
<h:form id="content">
<ui:include src="#{navigation.includePath}" />
</h:form>
導(dǎo)航類:
public class Navigation {
private String viewName;
public void goTo(String viewName) {
this.viewName = viewName;
}
public String getIncludePath() {
return resolvePath(viewName);
}
}
我見過類似的例子,但這當(dāng)然行不通.由于 ui:include
是一個(gè)標(biāo)記處理程序,因此在調(diào)用我的導(dǎo)航偵聽器之前很久就發(fā)生了包含.包括舊的 facelet,而不是新的.到目前為止,我明白了.
I have seen similar examples, but this doesn't work of course. As ui:include
is a taghandler, the include happens long before my navigation listener is invoked. The old facelet is included, instead of the new. So far I get it.
現(xiàn)在到頭疼的部分:如何基于 actionListener 動(dòng)態(tài)包含 facelet?我試圖將 facelet 包含在 preRender 事件中,并在 RENDER_RESPONSE 之前包含一個(gè) phaseListener.兩者都有效,但是在事件偵聽器中,我不能包含包含其他 preRender 事件的 facelet,并且在 phaseListener 中,在包含的 facelet 中單擊一些后,我會(huì)得到重復(fù)的 Id.但是,檢查組件樹告訴我,根本沒有重復(fù)的組件.也許這兩個(gè)想法一點(diǎn)都不好..
Now to the headache part: How can I dynamically include a facelet, based on an actionListener? I tried to include the facelet in a preRender event, and a phaseListener before RENDER_RESPONSE. Both work, but in the event listener I can't include a facelet which contains an other preRender event, and in the phaseListener I get duplicate Id's after some clicks in the included facelet. However, inspecting the component tree tells me, there are no duplicate components at all. Maybe these two ideas were not to good at all..
我需要一個(gè)解決方案,其中帶有 ui:include
的頁面或包含 facelet 的 Java 類不必知道將被包含的頁面,也不必知道確切的小路.以前有人解決過這個(gè)問題嗎?我該怎么做?
I need a solution, where the page with the ui:include
, or the Java class which includes the facelet, doesn't have to know the pages, which will be included, nor the exact path. Did anybody solve this problem before? How can I do it?
我正在使用 JSF 2.1 和 Mojarra 2.1.15
I am using JSF 2.1 and Mojarra 2.1.15
重現(xiàn)問題所需的只是這個(gè) bean:
All you need to reproduce the Problem is this bean:
@Named
public class Some implements Serializable {
private static final long serialVersionUID = 1L;
private final List<String> values = new ArrayList<String>();
public Some() {
values.add("test");
}
public void setInclude(String include) {
}
public List<String> getValues() {
return values;
}
}
這在您的索引文件中:
<h:head>
<h:outputScript library="javax.faces" name="jsf.js" />
</h:head>
<h:body>
<h:form id="topform">
<h:panelGroup id="container">
<my:include src="/test.xhtml" />
</h:panelGroup>
</h:form>
</h:body>
這在 text.xhtml 中
And this in text.xhtml
<ui:repeat value="#{some.values}" var="val">
<h:commandLink value="#{val}" action="#{some.setInclude(val)}">
<f:ajax render=":topform:container" />
</h:commandLink>
</ui:repeat>
這足以產(chǎn)生這樣的錯(cuò)誤:
That's enough to produce an error like this:
javax.faces.FacesException: Cannot add the same component twice: topform:j_id-549384541_7e08d92c
推薦答案
對(duì)于OmniFaces,我也試過通過創(chuàng)建一個(gè) <o:include>
作為 UIComponent
而不是 TagHandler
執(zhí)行 FaceletContext#includeFacelet()
在 encodeChildren()
方法.這樣,在恢復(fù)視圖階段會(huì)記住正確包含的 facelet,并且包含的??組件樹僅在渲染響應(yīng)階段發(fā)生變化,這正是我們想要實(shí)現(xiàn)的構(gòu)造.
For OmniFaces, I've also ever experimented with this by creating an <o:include>
as UIComponent
instead of a TagHandler
which does a FaceletContext#includeFacelet()
in the encodeChildren()
method. This way the right included facelet is remembered during restore view phase and the included component tree only changes during render response phase, which is exactly what we want to achieve this construct.
這是一個(gè)基本的啟動(dòng)示例:
Here's a basic kickoff example:
@FacesComponent("com.example.Include")
public class Include extends UIComponentBase {
@Override
public String getFamily() {
return "com.example.Include";
}
@Override
public boolean getRendersChildren() {
return true;
}
@Override
public void encodeChildren(FacesContext context) throws IOException {
getChildren().clear();
((FaceletContext) context.getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY)).includeFacelet(this, getSrc());
super.encodeChildren(context);
}
public String getSrc() {
return (String) getStateHelper().eval("src");
}
public void setSrc(String src) {
getStateHelper().put("src", src);
}
}
在.taglib.xml
中注冊(cè)如下:
<tag>
<tag-name>include</tag-name>
<component>
<component-type>com.example.Include</component-type>
</component>
<attribute>
<name>src</name>
<required>true</required>
<type>java.lang.String</type>
</attribute>
</tag>
這適用于以下視圖:
<h:outputScript name="fixViewState.js" />
<h:form>
<ui:repeat value="#{includeBean.includes}" var="include">
<h:commandButton value="Include #{include}" action="#{includeBean.setInclude(include)}">
<f:ajax render=":include" />
</h:commandButton>
</ui:repeat>
</h:form>
<h:panelGroup id="include">
<my:include src="#{includeBean.include}.xhtml" />
</h:panelGroup>
以及以下支持 bean:
And the following backing bean:
@ManagedBean
@ViewScoped
public class IncludeBean implements Serializable {
private List<String> includes = Arrays.asList("include1", "include2", "include3");
private String include = includes.get(0);
private List<String> getIncludes() {
return includes;
}
public void setInclude(String include) {
return this.include = include;
}
public String getInclude() {
return include;
}
}
(此示例需要包含文件 include1.xhtml
、include2.xhtml
和 include3.xhtml
位于相同的基本文件夾中主文件)
(this example expects include files include1.xhtml
, include2.xhtml
and include3.xhtml
in the same base folder as the main file)
fixViewState.js
可以在這個(gè)答案中找到:h:commandButton/h:commandLink 在第一次點(diǎn)擊時(shí)不起作用,僅在第二次點(diǎn)擊時(shí)起作用.為了修復(fù) JSF 問題 790 導(dǎo)致視圖狀態(tài)在有多個(gè) ajax 表單可以更新彼此的父級(jí).
The fixViewState.js
can be found in this answer: h:commandButton/h:commandLink does not work on first click, works only on second click. This script is mandatory in order to fix JSF issue 790 whereby the view state get lost when there are multiple ajax forms which update each other's parent.
另請(qǐng)注意,這樣每個(gè)包含文件在必要時(shí)都可以有自己的 <h:form>
,因此您不必將它放在包含周圍.
Also note that this way each include file can have its own <h:form>
when necessary, so you don't necessarily need to put it around the include.
這種方法在 Mojarra 中運(yùn)行良好,即使回發(fā)請(qǐng)求來自包含內(nèi)的表單,但它在 MyFaces 中很難失敗,初始請(qǐng)求期間已經(jīng)出現(xiàn)以下異常:
This approach works fine in Mojarra, even with postback requests coming from forms inside the include, however it fails hard in MyFaces with the following exception during initial request already:
java.lang.NullPointerException
at org.apache.myfaces.view.facelets.impl.FaceletCompositionContextImpl.generateUniqueId(FaceletCompositionContextImpl.java:910)
at org.apache.myfaces.view.facelets.impl.DefaultFaceletContext.generateUniqueId(DefaultFaceletContext.java:321)
at org.apache.myfaces.view.facelets.compiler.UIInstructionHandler.apply(UIInstructionHandler.java:87)
at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:49)
at org.apache.myfaces.view.facelets.tag.ui.CompositionHandler.apply(CompositionHandler.java:158)
at org.apache.myfaces.view.facelets.compiler.NamespaceHandler.apply(NamespaceHandler.java:57)
at org.apache.myfaces.view.facelets.compiler.EncodingHandler.apply(EncodingHandler.java:48)
at org.apache.myfaces.view.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:394)
at org.apache.myfaces.view.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:448)
at org.apache.myfaces.view.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:426)
at org.apache.myfaces.view.facelets.impl.DefaultFaceletContext.includeFacelet(DefaultFaceletContext.java:244)
at com.example.Include.encodeChildren(Include.java:54)
MyFaces 基本上會(huì)在視圖構(gòu)建結(jié)束時(shí)釋放 Facelet 上下文,使其在視圖渲染期間不可用,從而導(dǎo)致 NPE,因?yàn)閮?nèi)部狀態(tài)具有多個(gè)無效屬性.但是,可以在渲染期間添加單個(gè)組件而不是 Facelet 文件.我真的沒有時(shí)間調(diào)查這是我的錯(cuò)還是 MyFaces 的錯(cuò).這也是為什么它還沒有出現(xiàn)在 OmniFaces 中的原因.
MyFaces basically releases the Facelet context during end of view build time, making it unavailable during view render time, resulting in NPEs because the internal state has several nulled-out properties. It's however possible to add individual components instead of a Facelet file during render time. I didn't really have had the time to investigate if this is my fault or MyFaces' fault. That's also why it didn't end up in OmniFaces yet.
如果您仍然在使用 Mojarra,請(qǐng)隨意使用它.但是,我強(qiáng)烈建議在同一頁面上對(duì)所有可能的用例進(jìn)行徹底測試.Mojarra 有一些與狀態(tài)保存相關(guān)的怪癖,可能在使用此構(gòu)造時(shí)會(huì)失敗.
If you're using Mojarra anyway, feel free to use it. I however strongly recommend to test it thoroughly with all possible use cases on the very same page. Mojarra has some state saving related quirks which might fail when using this construct.
這篇關(guān)于使用 <ui:include> 的動(dòng)態(tài) ajax 導(dǎo)航的文章就介紹到這了,希望我們推薦的答案對(duì)大家有所幫助,也希望大家多多支持html5模板網(wǎng)!