Изменение дерева компонентов JSF в PhaseListener

У меня проблема.

Я реализовал PhaseListener, который предназначен для добавления класса стиля к любым компонентам UIInput в дереве, к которым прикреплены сообщения, и удаляет класс стиля, если к нему не прикреплены какие-либо сообщения.

PhaseListener запускается на этапе RENDER_RESPONSE и работает как в методах beforePhase, так и в afterPhase во время отладки. Во время отладки я обнаружил, что у beforePhase нет доступа к полному дереву компонентов, а у afterPhase есть. Однако любые изменения, сделанные в afterPhase, не отображаются.

Как мне это сделать? Я хочу, чтобы это было полностью на стороне сервера.

Спасибо,

Джеймс


person jamiebarrow    schedule 18.03.2010    source источник


Ответы (2)


Дерево компонентов JSF доступно только после времени сборки представления. Фаза RENDER_RESPONSE не обязательно является подходящим моментом для доступа к полному дереву компонентов JSF до его рендеринга. Во время первоначального запроса GET без каких-либо <f:viewAction> полное дерево компонентов доступно только в afterPhase, так как оно строится во время RENDER_RESPONSE. Во время обратной передачи полное дерево компонентов доступно на этапе beforePhase, однако, когда происходит переход к другому представлению, оно все равно будет изменено во время этапа RENDER_RESPONSE, поэтому любые изменения будут потеряны. .

Чтобы узнать точное время сборки представления, ответьте на вопрос Каково время сборки представления?

В основном вы хотите подключиться к «просмотру времени рендеринга», а не к beforePhase из RENDER_RESPONSE фазы. JSF предлагает несколько способов зацепиться за него:

  1. В каком-нибудь мастер-шаблоне прикрепите прослушиватель preRenderView к <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. Или реализуйте глобальный SystemEventListener для < a href="http://docs.oracle.com/javaee/7/api/javax/faces/event/PreRenderViewEvent.html" rel="nofollow noreferrer">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.
            // ...
        }
    
    }
    

    Чтобы запустить его, зарегистрируйте его, как показано ниже, в 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. Или укажите собственный ViewHandler, в котором вы выполнить задание в 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;
        }
    
    }
    

    Чтобы запустить его, зарегистрируйтесь, как показано ниже в faces-config.xml:

    <application>
        <view-handler>com.example.YourViewHandler</view-handler>
    </application>
    
  4. Или подключитесь к ViewDeclarationLanguage#renderView(), но это немного на грани, поскольку на самом деле он предназначен не для управления деревом компонентов, а для управления способом визуализации представления.


Не связанный с конкретной проблемой, все это не является правильным решением для конкретного функционального требования, как указано в вашем вопросе:

который предназначен для добавления класса стиля к любым компонентам UIInput в дереве, к которым прикреплены сообщения, и удаляет класс стиля, если к нему не прикреплены никакие сообщения

Вам действительно лучше перейти к решению на стороне клиента, а не манипулировать деревом компонентов (которое в конечном итоге окажется в состоянии компонента JSF!). Представьте себе случай ввода в повторяющихся компонентах, таких как <ui:repeat><h:inputText>. Физически в дереве есть только один входной компонент, а не несколько! Манипулирование классом стиля через UIInput#setStyleClass() будет представлено в каждой итерации.

Лучше всего посетить дерево компонентов, используя UIViewRoot#visitTree(), как показано ниже, и собрать все идентификаторы клиентов недопустимых входных компонентов (этот подход 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;
    }
});

А затем передайте invalidInputClientIds в виде массива JSON в JavaScript, который затем захватит их через document.getElementById() и изменит атрибут className.

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

В служебной библиотеке JSF OmniFaces есть <o:highlight>, который делает именно это.

person BalusC    schedule 10.11.2015
comment
Спасибо за это. На самом деле я не прикасался к JSF годами :), но, возможно, это полезно для других. - person jamiebarrow; 11.11.2015

Реализовано с помощью ViewHandler, однако это неэффективно. PhaseListener на этапе Render Response не имеет доступа к дереву компонентов.

person jamiebarrow    schedule 23.03.2010