开发者

Modifying JSF Component Tree in PhaseListener

开发者 https://www.devze.com 2022-12-23 21:31 出处:网络
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, and removes the style class if

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, and removes the style class if it doesn't have any messages attached to them.

The PhaseListener runs in the RENDER_RESPONSE phase, and does it's work in both the beforePhas开发者_如何学JAVAe and afterPhase methods while debugging. While debugging, I found that beforePhase doesn't have access to the full component tree, but afterPhase does. Any changes done in afterPhase aren't rendered though.

How do I go about this? I want this to be completely server side.

Thanks,

James


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.


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

0

精彩评论

暂无评论...
验证码 换一张
取 消