Modifying JSF Component Tree in PhaseListener
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:
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. // ... }
Or, implement a global
SystemEventListener
forPreRenderViewEvent
.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>
Or, provide a custom
ViewHandler
wherein you do the job inrenderView()
.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>
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.
精彩评论