Modifying JSF Component Tree in PhaseListener

后端 未结 2 2087
孤独总比滥情好
孤独总比滥情好 2021-01-25 07:40

I\'m having an issue.

I\'ve implemented a PhaseListener, which is meant to add a style class to any UIInput components in the tree that have messages attached to them, a

相关标签:
2条回答
  • 2021-01-25 08:11

    The JSF component tree is only available after the view build time. The RENDER_RESPONSE phase is not necessarily a good moment to have access to the full JSF component tree before it gets rendered. During an initial GET request without any <f:viewAction>, the full component tree is only available in the afterPhase as it's being built during the RENDER_RESPONSE. During a postback the full component tree is available in the beforePhase, however, when a navigation to a different view has taken place, then it would stil be changed during the RENDER_RESPONSE phase, so any modifications would get lost.

    To learn what exactly the view build time is, head to the question What's the view build time?

    You basically want to hook on "view render time" rather than beforePhase of RENDER_RESPONSE phase. JSF offers several ways to hook on it:

    1. In some master template, attach a preRenderView listener to <f:view>.

      <f:view ...>
          <f:event type="preRenderView" listener="#{bean.onPreRenderView}" />
          ...
      </f:view>
      
      public void onPreRenderView(ComponentSystemEvent event) {
          UIViewRoot view = (UIViewRoot) event.getSource();
          // The view is the component tree. Just modify it here accordingly.
          // ...
      }        
      
    2. Or, implement a global SystemEventListener for PreRenderViewEvent.

      public class YourPreRenderViewListener implements SystemEventListener {
      
          @Override
          public boolean isListenerForSource(Object source) {
              return source instanceof UIViewRoot;
          }
      
          @Override
          public void processEvent(SystemEvent event) throws AbortProcessingException {
              UIViewRoot view = (UIViewRoot) event.getSource();
              // The view is the component tree. Just modify it here accordingly.
              // ...
          }
      
      }
      

      To get it to run, register it as below in faces-config.xml:

      <application>
          <system-event-listener>
              <system-event-listener-class>com.example.YourPreRenderViewListener</system-event-listener-class>
              <system-event-class>javax.faces.event.PreRenderViewEvent</system-event-class>
          </system-event-listener>
      </application>
      
    3. Or, provide a custom ViewHandler wherein you do the job in renderView().

      public class YourViewHandler extends ViewHandlerWrapper {
      
          private ViewHandler wrapped;
      
          public YourViewHandler(ViewHandler wrapped) {
              this.wrapped = wrapped;
          }
      
          @Override
          public void renderView(FacesContext context, UIViewRoot view) {
              // The view is the component tree. Just modify it here accordingly.
              // ...
      
              // Finally call super so JSF can do the rendering job.
              super.renderView(context, view);
          }
      
          @Override
          public ViewHandler getWrapped() {
              return wrapped;
          }
      
      }
      

      To get it to run, register as below in faces-config.xml:

      <application>
          <view-handler>com.example.YourViewHandler</view-handler>
      </application>
      
    4. Or, hook on ViewDeclarationLanguage#renderView(), but this is a bit on the edge as it isn't really intented to manipulate the component tree, but to manipulate how to render the view.


    Unrelated to the concrete problem, this all is not the right solution for the concrete functional requirement as stated in your question:

    which is meant to add a style class to any UIInput components in the tree that have messages attached to them, and removes the style class if it doesn't have any messages attached to them

    You'd really better head for a client side solution rather than manipulating the component tree (which would end up in JSF component state!). Imagine the case of inputs in iterating components such as <ui:repeat><h:inputText>. There's physically only one input component in the tree, not multiple! Manipulating the style class via UIInput#setStyleClass() would get presented in every iteration round.

    You'd best visit the component tree using UIViewRoot#visitTree() as below and collect all client IDs of invalid input components (this visitTree() approach will transparently take iterating components into account):

    Set<String> invalidInputClientIds = new HashSet<>();
    view.visitTree(VisitContext.createVisitContext(context, null, EnumSet.of(VisitHint.SKIP_UNRENDERED)), new VisitCallback() {
    
        @Override
        public VisitResult visit(VisitContext context, UIComponent component) {
            if (component instanceof UIInput) {
                UIInput input = (UIInput) component;
    
                if (!input.isValid()) {
                    invalidInputClientIds.add(input.getClientId(context.getFacesContext()));
                }
            }
    
            return VisitResult.ACCEPT;
        }
    });
    

    And then thereafter pass invalidInputClientIds in flavor of a JSON array to JavaScript which will then grab them via document.getElementById() and alter the className attribute.

    for (var i = 0; i < invalidInputClientIds.length; i++) {
        var invalidInput = document.getElementById(invalidInputClientIds[i]);
        invalidInput.className += ' error';
    }
    

    The JSF utility library OmniFaces has a <o:highlight> component which does exactly this.

    0 讨论(0)
  • 2021-01-25 08:19

    Implemented using a ViewHandler, however it's not efficient. PhaseListener in Render Response phase doesn't have access to the component tree.

    0 讨论(0)
提交回复
热议问题