修改 PhaseListener 中的 JSF 组件树

发布于 2024-08-25 18:42:14 字数 311 浏览 13 评论 0 原文

我有一个问题。

我实现了一个 PhaseListener,它的目的是向树中附加有消息的任何 UIInput 组件添加样式类,如果没有附加任何消息,则删除该样式类。

PhaseListener 在 RENDER_RESPONSE 阶段运行,并在调试时在 beforePhase 和 afterPhase 方法中工作。在调试时,我发现 beforePhase 无法访问完整的组件树,但 afterPhase 可以。不过,在 afterPhase 中所做的任何更改都不会呈现。

我该怎么办?我希望这完全是服务器端的。

谢谢,

詹姆斯

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 beforePhase 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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

悲欢浪云 2024-09-01 18:42:14

JSF 组件树仅在视图构建时间之后才可用。 RENDER_RESPONSE 阶段不一定是在渲染之前访问完整 JSF 组件树的好时机。在没有任何 的初始 GET 请求期间,完整的组件树仅在 afterPhase 中可用,因为它是在 RENDER_RESPONSERENDER_RESPONSE 期间构建的代码>.在回发期间,完整的组件树在 beforePhase 中可用,但是,当导航到不同的视图时,它仍然会在期间发生更改。 >RENDER_RESPONSE 阶段,因此任何修改都会丢失。

要了解视图构建时间到底是多少,请转到问题视图构建时间是多少?

您基本上想要挂钩“查看渲染时间”,而不是 RENDER_RESPONSE 阶段的 beforePhase 。 JSF 提供了几种挂接它的方法:

  1. 在某些主模板中,将 preRenderView 侦听器附加到

    <前><代码>

    ...

    public void onPreRenderView(ComponentSystemEvent event) {
        UIViewRoot 视图 = (UIViewRoot) event.getSource();
        // 视图是组件树。这里只需修改一下就可以了。
        // ...
    }        
    

  2. 或者,实现一个全局 SystemEventListener PreRenderViewEvent

    公共类 YourPreRenderViewListener 实现 SystemEventListener {
    
        @覆盖
        公共布尔 isListenerForSource(对象源){
            返回 UIViewRoot 的源实例;
        }
    
        @覆盖
        公共无效processEvent(系统事件事件)抛出AbortProcessingException {
            UIViewRoot 视图 = (UIViewRoot) event.getSource();
            // 视图是组件树。这里只需修改一下就可以了。
            // ...
        }
    
    }
    

    要使其运行,请按如下方式在 faces-config.xml 中注册它:

    <前><代码><应用程序>
    <系统事件监听器>
    com.example.YourPreRenderViewListener
    <系统事件类>javax.faces.event.PreRenderViewEvent

  3. 或者,提供自定义 ViewHandler 您在其中执行的工作 renderView( )

    public class YourViewHandler extends ViewHandlerWrapper {
    
        私有 ViewHandler 包装;
    
        公共 YourViewHandler(ViewHandler 包装){
            this.wrapped = 包裹;
        }
    
        @覆盖
        公共无效renderView(FacesContext上下文,UIViewRoot视图){
            // 视图是组件树。这里只需修改一下就可以了。
            // ...
    
            // 最后调用 super 以便 JSF 可以完成渲染工作。
            super.renderView(上下文,视图);
        }
    
        @覆盖
        公共 ViewHandler getWrapped() {
            包裹退回;
        }
    
    }
    

    要使其运行,请在faces-config.xml中进行如下注册:

    <前><代码><应用程序>
    com.example.YourViewHandler;

  4. 或者,挂接 ViewDeclarationLanguage#renderView()< /a>,但这有点边缘,因为它并不是真正旨在操纵组件树,而是操纵如何渲染视图。


与具体问题无关,这一切都不是您问题中所述的具体功能要求的正确解决方案:

这意味着将样式类添加到树中附加了消息的任何 UIInput 组件,如果没有附加任何消息,则删除样式类

寻求客户端解决方案,而不是操作组件树(这最终会处于 JSF 组件状态!)。想象一下迭代组件中输入的情况,例如 。树中实际上只有一个输入组件,而不是多个!通过 UIInput#setStyleClass() 操作样式类将在每次迭代中呈现。

您最好使用 UIViewRoot#visitTree() 访问组件树,如下所示,并收集无效输入组件的所有客户端 ID(此 visitTree() 方法将透明地采用迭代组件考虑):

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;
    }
});

然后将 JSON 数组形式的 invalidInputClientIds 传递给 JavaScript,然后 JavaScript 将通过 document.getElementById() 获取它们并更改 className< /代码> 属性。

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

JSF 实用程序库 OmniFaces 有一个 组件正是这样做的。

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.

只有一腔孤勇 2024-09-01 18:42:14

使用 ViewHandler 实现,但效率不高。渲染响应阶段的 PhaseListener 无法访问组件树。

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文