JSF状态保存和具有动态添加子组件的自定义组件

14

我正在开发一个JSF自定义组件,其目的是封装另一个组件(即PrimeFaces表格)并添加自定义行为。例如,它支持从基础数据或某些属性动态创建PrimeFaces列。此外,它还支持在XHTML中声明附加的PrimeFaces列,这些列也应添加到封装的PrimeFaces表格中。

考虑以下示例:

<my:table id="table" fields="title,label,value,additional">
    <primefaces:column id="additional">
        some content
    </primefaces:column>
</my:table>

我的自定义组件在渲染时从fields属性动态创建PrimeFaces列。然后将其所有的column子元素移动到PrimeFaces表格中,所以在渲染完组件树后,它的样子如下:

my:table id="table"
|---primefaces:table id="table_table"
    |---primefaces:column id="title"
    |---primefaces:column id="label"
    |---primefaces:column id="value"
    |---primefaces:column id="additional"
在首次渲染时,这个工作正常。但是,当我执行组件的AJAX更新后,就会出现以下异常:
javax.faces.FacesException: Cannot remove the same component twice: table:additional
    at com.sun.faces.context.StateContext$DynamicAddRemoveListener.handleAddRemoveWithAutoPrune(StateContext.java:761)
    at com.sun.faces.context.StateContext$DynamicAddRemoveListener.handleRemove(StateContext.java:629)
    at com.sun.faces.context.StateContext$AddRemoveListener.processEvent(StateContext.java:342)
    at com.sun.faces.context.StateContext$DynamicAddRemoveListener.processEvent(StateContext.java:565)
    at javax.faces.event.SystemEvent.processListener(SystemEvent.java:108)
    at javax.faces.event.ComponentSystemEvent.processListener(ComponentSystemEvent.java:118)
    at com.sun.faces.application.ApplicationImpl.processListenersAccountingForAdds(ApplicationImpl.java:2218)
    at com.sun.faces.application.ApplicationImpl.invokeViewListenersFor(ApplicationImpl.java:2036)
    at com.sun.faces.application.ApplicationImpl.publishEvent(ApplicationImpl.java:290)
    at com.sun.faces.application.ApplicationImpl.publishEvent(ApplicationImpl.java:245)
    at javax.faces.application.ApplicationWrapper.publishEvent(ApplicationWrapper.java:726)
    at javax.faces.component.UIComponentBase.disconnectFromView(UIComponentBase.java:2275)
    at javax.faces.component.UIComponentBase.doPreRemoveProcessing(UIComponentBase.java:1939)
    at javax.faces.component.UIComponentBase.setParent(UIComponentBase.java:437)
    at javax.faces.component.UIComponentBase$ChildrenList.remove(UIComponentBase.java:2757)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.adjustIndexOfDynamicChildren(ComponentTagHandlerDelegateImpl.java:283)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.apply(ComponentTagHandlerDelegateImpl.java:223)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.apply(DelegatingMetaTagHandler.java:120)
    at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:95)
    at com.sun.faces.facelets.tag.ui.DefineHandler.applyDefinition(DefineHandler.java:106)
    at com.sun.faces.facelets.tag.ui.CompositionHandler.apply(CompositionHandler.java:206)
    at com.sun.faces.facelets.impl.DefaultFaceletContext$TemplateManager.apply(DefaultFaceletContext.java:395)
    at com.sun.faces.facelets.impl.DefaultFaceletContext.includeDefinition(DefaultFaceletContext.java:366)
    at com.sun.faces.facelets.tag.ui.InsertHandler.apply(InsertHandler.java:111)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.applyNextHandler(DelegatingMetaTagHandler.java:137)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.apply(ComponentTagHandlerDelegateImpl.java:202)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.apply(DelegatingMetaTagHandler.java:120)
    at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:95)
    at com.sun.faces.facelets.tag.ui.CompositionHandler.apply(CompositionHandler.java:194)
    at com.sun.faces.facelets.compiler.NamespaceHandler.apply(NamespaceHandler.java:93)
    at com.sun.faces.facelets.compiler.EncodingHandler.apply(EncodingHandler.java:87)
    at com.sun.faces.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:312)
    at com.sun.faces.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:371)
    at com.sun.faces.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:350)
    at com.sun.faces.facelets.impl.DefaultFaceletContext.includeFacelet(DefaultFaceletContext.java:199)
    at com.sun.faces.facelets.tag.ui.IncludeHandler.apply(IncludeHandler.java:124)
    at com.sun.faces.facelets.tag.ui.InsertHandler.apply(InsertHandler.java:116)
    at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:95)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.applyNextHandler(DelegatingMetaTagHandler.java:137)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.apply(ComponentTagHandlerDelegateImpl.java:202)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.apply(DelegatingMetaTagHandler.java:120)
    at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:95)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.applyNextHandler(DelegatingMetaTagHandler.java:137)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.apply(ComponentTagHandlerDelegateImpl.java:202)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.apply(DelegatingMetaTagHandler.java:120)
    at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:95)
    at com.sun.faces.facelets.tag.ui.DefineHandler.applyDefinition(DefineHandler.java:106)
    at com.sun.faces.facelets.tag.ui.CompositionHandler.apply(CompositionHandler.java:206)
    at com.sun.faces.facelets.impl.DefaultFaceletContext$TemplateManager.apply(DefaultFaceletContext.java:395)
    at com.sun.faces.facelets.impl.DefaultFaceletContext.includeDefinition(DefaultFaceletContext.java:366)
    at com.sun.faces.facelets.tag.ui.InsertHandler.apply(InsertHandler.java:111)
    at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:95)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.applyNextHandler(DelegatingMetaTagHandler.java:137)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.apply(ComponentTagHandlerDelegateImpl.java:202)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.apply(DelegatingMetaTagHandler.java:120)
    at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:95)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.applyNextHandler(DelegatingMetaTagHandler.java:137)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.apply(ComponentTagHandlerDelegateImpl.java:202)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.apply(DelegatingMetaTagHandler.java:120)
    at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:95)
    at com.sun.faces.facelets.tag.jsf.core.ViewHandler.apply(ViewHandler.java:225)
    at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:95)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.applyNextHandler(DelegatingMetaTagHandler.java:137)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.apply(ComponentTagHandlerDelegateImpl.java:202)
    at javax.faces.view.facelets.DelegatingMetaTagHandler.apply(DelegatingMetaTagHandler.java:120)
    at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:95)
    at com.sun.faces.facelets.compiler.NamespaceHandler.apply(NamespaceHandler.java:93)
    at com.sun.faces.facelets.compiler.EncodingHandler.apply(EncodingHandler.java:87)
    at com.sun.faces.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:312)
    at com.sun.faces.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:371)
    at com.sun.faces.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:350)
    at com.sun.faces.facelets.impl.DefaultFaceletContext.includeFacelet(DefaultFaceletContext.java:199)
    at com.sun.faces.facelets.tag.ui.CompositionHandler.apply(CompositionHandler.java:174)
    at com.sun.faces.facelets.compiler.NamespaceHandler.apply(NamespaceHandler.java:93)
    at com.sun.faces.facelets.compiler.EncodingHandler.apply(EncodingHandler.java:87)
    at com.sun.faces.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:312)
    at com.sun.faces.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:371)
    at com.sun.faces.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:350)
    at com.sun.faces.facelets.impl.DefaultFaceletContext.includeFacelet(DefaultFaceletContext.java:199)
    at com.sun.faces.facelets.tag.ui.CompositionHandler.apply(CompositionHandler.java:174)
    at com.sun.faces.facelets.compiler.NamespaceHandler.apply(NamespaceHandler.java:93)
    at com.sun.faces.facelets.compiler.EncodingHandler.apply(EncodingHandler.java:87)
    at com.sun.faces.facelets.impl.DefaultFacelet.apply(DefaultFacelet.java:161)
    at com.sun.faces.application.view.FaceletViewHandlingStrategy.buildView(FaceletViewHandlingStrategy.java:1006)
    at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:99)
    at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:101)
    at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:219)
    at javax.faces.webapp.FacesServlet.service(FacesServlet.java:647)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:230)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165)
    at de.gebit.trend.servlet.security.AuthorizationFilter.doFilter(AuthorizationFilter.java:269)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:108)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:620)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:349)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:783)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:789)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1455)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Unknown Source)

因此,只有从我的表移动到PrimeFaces表中的column才会创建此错误。其他列不会被重新创建,因为在渲染期间,我使用存储在StateHelper中的实例变量来指示列已经被创建。

我有点理解此异常的来源,并且知道它与JSF保存完整组件树有关,当JSF还原视图时,保存的状态与XHTML不一致。我不知道的是如何解决此问题。

请问有人能够向我解释状态保存机制的工作原理,特别是与动态添加子元素的结合使用以及如何避免此异常吗?

更新(2017年10月2日)

我创建了一个小的样本项目,没有使用之前使用的任何其他框架,可以在我的GitHub个人资料中找到。之前使用的一个主要框架正在处理安装在StateContext中用于重放动态操作的AddRemoveListeners。为了避免对我的问题产生影响并创建可复制的环境,我将其删除。

我现在观察到的行为略有不同(不再有异常),取决于是否启用/禁用部分状态保存以及我用于移动primefaces:column的方法:

在所有情况下,表的第一次呈现都可以正常工作。然后,我通过提交分页请求执行后端请求。此时,行为在某些情况下是错误的。

启用部分状态保存

启用部分状态保存后,分页不起作用。我没有收到异常,但类似于以下警告:

Feb 10, 2017 4:33:11 PM com.sun.faces.application.view.FaceletPartialStateManagementStrategy saveDynamicActions
WARNUNG: Unable to save dynamic action with clientId 'form:table:table_table:additional' because the UIComponent cannot be found

此警告出现在动态创建或移动到primefaces:table的每个组件中,或者是其中一个子组件。

禁用部分状态保存

禁用部分状态保存后,分页可以正常工作,但取决于何时移动自定义的primefaces:column,会显示不同的行为。

在渲染响应阶段移动`column`

当在渲染响应阶段(如encodeXxx)中移动primefaces:column时,一切都正常工作。所有列都按正确顺序和正确值排列,并且分页正常工作。

使用`PostAddToViewEvent`移动`column`

根据@BalusC的建议,使用这种方法时,移动的primefaces:column会在分页时消失。多次调用PostAddToViewEvent,并且在处理此事件时移动column,但是在渲染时,它已经消失,只剩下之前创建的三个column

此时,我已经感到非常困惑。这是Mojarra还是Primefaces中的一个错误,还是我做错了什么?这种行为在JSF中是否可能发生?

2个回答

11

JSF状态管理会记录组件树的动态操作,以便在后续处理postback的"恢复视图"时,确保它与上一个生成post表单的请求的"呈现响应"期间完全相同。

你遇到的异常,

javax.faces.FacesException: Cannot remove the same component twice: table:additional

基本上告诉你,你已经将相同的组件两次添加到同一个父组件中。

换句话说,你从组件树中获取了所需的组件,然后将其添加到所需的父组件中。但是,根据给定的异常,在那个时刻它已经被附加到了所需的父组件!实际上,你执行了一个无操作。但是JSF通过postback在同一个视图上记住了每个动态组件的添加/删除,即使它实际上是一个无操作。这部分可能反过来是JSF实现本身的错误,但首先不应该在组件已经在所需位置时移动组件。

一个快速的解决方法是通过UIComponent#getParent()检查组件的父级是否已经是所需的父级,如果是,则跳过getChildren().add()调用。

if (!componentToMove.getParent().equals(targetParent)) {
    targetParent.getChildren().add(componentToMove);
}

一个技巧是将UIComponent#setInView()设置为false,这样JSF就不会记住动态操作了。

componentToMove.setInView(false);
targetParent.getChildren().add(componentToMove);
componentToMove.setInView(true);

// NOTE: with MyFaces, call setInView() on componentToMove.getParent() instead.

使用此方法时应当谨慎,同时查看其javadoc

然而,执行组件树操作的最自然方式是使用postAddtoViewEvent监听器而不是在encodeXxx()方法期间进行操作。

@ListenerFor(systemEventClass=PostAddToViewEvent.class)
public class YourComponent extends SomeUIComponent {

@Override
public void processEvent(ComponentSystemEvent event) {
    if (event instanceof PostAddToViewEvent) {
        targetParent.getChildren().add(componentToMove);
    }
}

3
javax.faces.PARTIAL_STATE_SAVING设置为false时,一切都按预期工作,不会出现异常。 - jessepeng
我在我的代码中看到了其他类似的Mojarra问题,通过将部分状态保存设置为false解决了这些问题。我会看看能否找到它们。 - Kukeltje
<p:dataTable> 是动态创建的还是包含在 <my:table> 中的? - BalusC
1
这很可能是你问题的原因。UIComponent 实例不属于 JSF 状态。要么将其作为标签文件使用,例如 <p:dataTable><ui:insert/></p:dataTable>,要么让它继承自 DataTable - BalusC
“setTransient(true)” 不应该禁用状态帮助器吗? - dforce
显示剩余13条评论

5
简单来说,这是因为使用了另一个框架导致的。该框架覆盖了JSF的状态保存机制,并用自定义机制替换掉。然而,实现有缺陷,没有正确处理负责保存动态组件操作的DynamicAddRemoveListener。他们修复了这个bug,现在它可以正常工作了。
但是,有几件事情是必须要注意修复我所需要的组件的:
首先,@BalusC指向了我移动自定义JSF组件中子组件的正确方式:应该使用PostAddToView事件监听器。
@ListenerFor(systemEventClass=PostAddToViewEvent.class)
public class YourComponent extends SomeUIComponent {

     @Override
     public void processEvent(ComponentSystemEvent event) {
          if (event instanceof PostAddToViewEvent) {
              targetParent.getChildren().add(componentToMove);
          }
     }
}
然而,这种方法的缺点是组件属性此时不会被设置。因此,如果需要这些属性,则只能在渲染响应阶段创建/移动组件。
其次,自定义JSF组件的子组件不应保存在StateHelper中。它们应该在每个请求上重新创建,以便JSF在重放动态操作时找到这些组件。
第三,动态创建的子组件的ID(如果设置)应始终在创建组件本身时设置。我的自定义组件仅在渲染响应阶段设置其子组件的ID,因此当JSF尝试重放动态操作时,它无法找到相应的组件。这是解决上面提到的“启用部分状态保存”问题的解决方案。
因此,在进行所有这些适应和修复其他框架之后,现在我的组件最终按预期工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接