问题描述
假设我想在我的应用程序中导航,并动态包含不同的 facelet 页面.我有一个这样的 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>
导航类:
public class Navigation {
private String viewName;
public void goTo(String viewName) {
this.viewName = viewName;
}
public String getIncludePath() {
return resolvePath(viewName);
}
}
我见过类似的例子,但这当然行不通.由于 ui:include
是一个标记处理程序,因此在调用我的导航侦听器之前很久就发生了包含.包括旧的 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.
现在到头疼的部分:如何基于 actionListener 动态包含 facelet?我试图将 facelet 包含在 preRender 事件中,并在 RENDER_RESPONSE 之前包含一个 phaseListener.两者都有效,但是在事件侦听器中,我不能包含包含其他 preRender 事件的 facelet,并且在 phaseListener 中,在包含的 facelet 中单击一些后,我会得到重复的 Id.但是,检查组件树告诉我,根本没有重复的组件.也许这两个想法一点都不好..
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..
我需要一个解决方案,其中带有 ui:include
的页面或包含 facelet 的 Java 类不必知道将被包含的页面,也不必知道确切的小路.以前有人解决过这个问题吗?我该怎么做?
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
重现问题所需的只是这个 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>
这足以产生这样的错误:
That's enough to produce an error like this:
javax.faces.FacesException: Cannot add the same component twice: topform:j_id-549384541_7e08d92c
推荐答案
对于OmniFaces,我也试过通过创建一个 <o:include>
作为 UIComponent
而不是 TagHandler
执行 FaceletContext#includeFacelet()
在 encodeChildren()
方法.这样,在恢复视图阶段会记住正确包含的 facelet,并且包含的组件树仅在渲染响应阶段发生变化,这正是我们想要实现的构造.
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.
这是一个基本的启动示例:
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
中注册如下:
<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
可以在这个答案中找到:h:commandButton/h:commandLink 在第一次点击时不起作用,仅在第二次点击时起作用.为了修复 JSF 问题 790 导致视图状态在有多个 ajax 表单可以更新彼此的父级.
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.
另请注意,这样每个包含文件在必要时都可以有自己的 <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 中运行良好,即使回发请求来自包含内的表单,但它在 MyFaces 中很难失败,初始请求期间已经出现以下异常:
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 基本上会在视图构建结束时释放 Facelet 上下文,使其在视图渲染期间不可用,从而导致 NPE,因为内部状态具有多个无效属性.但是,可以在渲染期间添加单个组件而不是 Facelet 文件.我真的没有时间调查这是我的错还是 MyFaces 的错.这也是为什么它还没有出现在 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,请随意使用它.但是,我强烈建议在同一页面上对所有可能的用例进行彻底测试.Mojarra 有一些与状态保存相关的怪癖,可能在使用此构造时会失败.
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.
这篇关于使用 <ui:include> 的动态 ajax 导航的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!