JSF Сохранение состояния и пользовательские компоненты с динамически добавленными детьми

Я разрабатываю пользовательский компонент JSF. Этот компонент имеет целью инкапсулировать другой компонент (а именно таблицу PrimeFaces) и добавить к нему настроенное поведение. Например, одна из функций, которые она поддерживает, это динамически создавать столбцы PrimeFaces из базовых данных или из определенных атрибутов. Кроме того, он поддерживает объявление дополнительных столбцов PrimeFaces в XHTML, которые также должны быть добавлены в таблицу Encapsulated PrimeFaces.

Рассмотрим этот пример:

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

Мой пользовательский компонент динамически создает столбцы PrimeFaces из атрибута fields во время рендеринга. Затем он перемещает всех своих дочерних элементов 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)

Итак, только эта column, которая была перенесена из моей таблицы в таблицу PrimeFaces, создает эту ошибку. Другие столбцы не воссоздаются, потому что во время рендеринга я использую переменную экземпляра, хранящуюся в StateHelper, чтобы указать, что столбцы уже созданы.

Я как-то понимаю, откуда это исключение, и что он имеет какое-то отношение к JSF, сохраняющему полное дерево компонентов, и когда JSF восстанавливает представление, сохраненное состояние несовместимо с XHTML. То, что я не знаю, как решить эту проблему.

Может ли кто-нибудь объяснить мне, как этот механизм экономии состояний работает точно, особенно в сочетании с динамически добавленными детьми и как избежать этого исключения?

ОБНОВЛЕНИЕ (10.02.2017)

Я создал небольшой образец проекта без дополнительных фреймворков, которые раньше использовались. Его можно найти на моем профиле GitHub. Одна из основных фреймворков, использовавшаяся ранее, была связана с AddRemoveListeners, установленным в StateContext для воспроизведения динамических действий. Чтобы это не повлияло на мою проблему и создать воспроизводимую среду, я удалил их.

Поведение, которое я наблюдаю сейчас, немного отличается (больше нет исключений) и зависит от того, включено ли /strong > сохранение частичного состояния и метод, который я использую для перемещения 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.

Перемещение столбца во время фазы ответа рендера

Когда primefaces:column перемещается во время фазы ответа рендеринга, например, в encodeXxx все работает нормально. Все столбцы находятся в правильном порядке, а правильные значения и пейджинг работают отлично.

Перемещение столбца с помощью `PostAddToViewEvent`

При использовании этого подхода, в соответствии с предложением @BalusC, перемещенный primefaces:column исчезает при пейджинге. PostAddToViewEvent вызывается несколько раз, а column перемещается во время обработки этого события, однако при рендеринге он исчезает, и только три ранее созданные column все еще существуют.

В этот момент я более чем смущен. Это ошибка в Mojarra или в Primefaces, или я делаю что-то неправильно? Возможно ли такое поведение с JSF?

Ответ 1

Простым ответом является то, что используется другая инфраструктура. Эта структура перезаписывает механизм сохранения состояния JSF и заменяет его на собственный механизм. Однако реализация была ошибочной и не заботилась о DynamicAddRemoveListener, ответственном за правильное сохранение действий динамических компонентов. Они исправили ошибку, теперь она отлично работает.

Однако для исправления моего компонента я хотел бы указать несколько вещей:

Во-первых, @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 находил эти компоненты при воспроизведении динамических действий.

В-третьих, идентификатор динамически созданных дочерних компонентов (если установлен) всегда должен устанавливаться, когда компонент создается сам. Мой пользовательский компонент устанавливал идентификатор своих дочерних компонентов только во время фазы ответа рендеринга, поэтому, когда JSF пытался воспроизвести динамические действия, он не смог найти соответствующие компоненты. Это было решением проблемы, упомянутой выше в разделе Разрешено сохранение частичного состояния.

Итак, со всеми этими адаптациями и исправлением для другой структуры, теперь, наконец, мой компонент работает по своему желанию.

Ответ 2

Управление состоянием JSF запоминает динамические манипуляции с деревом компонентов, чтобы гарантировать, что после просмотра восстановления после точно такой же, как и во время отклика предыдущего запроса, который сгенерировал почтовая форма.

Исключение, которое вы получили,

javax.faces.FacesException: не удается удалить один и тот же компонент дважды: table: дополнительная

в основном говорит, что вы добавили один и тот же компонент дважды к одному и тому же родителю.

Другими словами, вы получили желаемый компонент из дерева компонентов и затем добавили его в желаемый родитель. Однако, согласно данному исключению, именно в этот момент уже привязан к желаемому родительскому элементу! Фактически, вы выполнили no-op. Но JSF запоминает каждый динамический компонент, добавляемый/удаляемый через обратную связь на одном и том же представлении, хотя он эффективно не работает. Эта часть, в свою очередь, может быть ошибкой в ​​реализации 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);
    }
}