Интеграция Primefaces в приложение на Spring Boot. Часть 2 — Готовим контекстное меню для главной страницы

от автора

В первой части статьи мы разобрали создание проекта на Spring и Primefaces и генерацию пустой главной страницы. Помимо этого, есть еще несколько полезных настроек, которые помогут вам улучшить работу приложения, покажем некоторые из них. Как обычно в приложениях Spring, настройки эти помещаются в файл application.properties:

joinfaces.jsf.state-saving-method=client 

Настройка JSF показывает, где будут храниться состояния UI — на стороне сервера или на стороне клиента. У каждого метода есть достоинства и недостатки. При хранении на стороне клиента создается меньше нагрузки на сервер, состояние хранится в дополнительном скрытом поле input в браузере. Кроме того, состояние не теряется при ошибках связи с сервером, что вполне вероятно будет происходить по разным причинам, например, у меня на develop стенде при разработке такое происходило из-за очень малых выделенных ресурсов сервера для этой задачи. В результате компоненты на странице могут зависать, и требовалось обновить страницу, чтобы возобновить их работу. Перевод этого параметра из server в client решил для меня эту проблему.

joinfaces.mojarra.number-of-logical-views=10000000 joinfaces.mojarra.number-of-views-in-session=10000000 joinfaces.myfaces.number-of-sequential-views-in-session=10000000 joinfaces.myfaces.client-view-state-timeout=600 spring.session.timeout=360000 

Эти настройки относятся к разным таймаутам хранения состояний, их я просто сделал побольше, чтобы состояние на клиенте не требовалось обновлять в течение одного рабочего дня или чуть больше

joinfaces.jsf.partial-state-saving=true 

Эта настройка позволяет JSF обновлять состояние не всей страницы, а только одного компонента или нескольких связанных компонентов, экономит ресурсы

joinfaces.mojarra.allow-text-children=true 

Настройка включает рендеринг дочерних элементов для h:inputText и h.outputText, требуется для работы некоторых компонентов в формах, как мы увидим в дальнейшем.

Перейдем теперь непосредственно к заполнению главной страницы компонентами. Рассмотрю только основные из них, мелкие компоненты, не требующие каких-либо особенных настроек вы просто увидите ниже в полном тексте страницы, полагаю, разобраться с ними у вас не составит труда

Я буду делать самый простой макет страницы, он будет включать в себя боковое меню и область с данными, которая будет обновляться при переходе по разным пунктам меню. Для реализации бокового меню разместим на странице компонент Tree ContextMenu и напишем класс компонента для него. Теперь наша главная страница будет выглядеть вот так:

<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"       xmlns:h="http://xmlns.jcp.org/jsf/html"       xmlns:f="http://xmlns.jcp.org/jsf/core"       xmlns:ui="http://xmlns.jcp.org/jsf/facelets"       xmlns:p="http://primefaces.org/ui">  <f:view contentType="text/html;charset=UTF-8" encoding="UTF-8">     <h:head>         <h:outputStylesheet library="css" name="styles.css"/>         <meta charset="utf-8" />         <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />         <title>Заголовок страницы</title>     </h:head>     <h:body>         <h:form>             <div class="card">                 <div id="top-panel">                     <p:panel header="Оглавление на странице">                         <f:facet name="actions">                             <p:commandLink                                     styleClass="ui-corner-all ui-state-default">                                 <p:graphicImage library="static" name="logo.gif" styleClass="ui-icon ui-icon-help" />                             </p:commandLink>                         </f:facet>                     </p:panel>                 </div>                 <div id="middle-panel">                     <p:splitter>                         <p:splitterPanel :size="20"                                          styleClass="flex align-items-center justify-content-center flex-error-left-1">                             <p:tree id="docs" value="#{treeContextMenuView.root}" var="doc" selectionMode="single"                                     selection="#{treeContextMenuView.selectedNode}" dynamic="true">                                 <p:treeNode expandedIcon="pi pi-folder-open" collapsedIcon="pi pi-folder">                                     <h:outputText value="#{doc.name}"/>                                 </p:treeNode>                                 <p:treeNode type="ips" icon="pi pi-folder">                                     <h:outputText value="#{doc.name}"/>                                 </p:treeNode>                                 <p:treeNode id="testid" type="contragent" icon="pi pi-file">                                     <h:outputText value="#{doc.name}"/>                                 </p:treeNode>                                 <p:ajax event="select"                                         listener="#{treeContextMenuView.setSrc()}" />                             </p:tree>                         </p:splitterPanel>                         <p:splitterPanel :size="80"                                          styleClass="flex align-items-center justify-content-center flex-error-right-1">                             <h:panelGroup id="list">                                 <h:panelGroup rendered="true">                                     <ui:include src="#{treeContextMenuView.getSrc()}" />                                 </h:panelGroup>                             </h:panelGroup>                         </p:splitterPanel>                     </p:splitter>                 </div>             </div>         </h:form>     </h:body> </f:view>  </html> 

Вы видите, что в различных элементах на странице повсюду упоминается некоторая ссылка, включающая в себя строку treeContextMenuView. Это ни что иное, как ссылка на имя управляемого бина компонента, который вызывается на этой странице и используется для получения данных из компонента и рендеринга отображаемых данных на странице. Никакой однозначной связи «страница — компонент» не существует, как это могло бы показаться на первый взгляд. Никто не мешает вам на одной странице обращаться по именам к совершенно разным компонентам, связанным или не связанным между собой. Primefaces сам найдет нужные компоненты в регистре и обработает соответствующим образом. Просто на данном этапе разработки я использовал только один активный компонент, но далее их будет больше. Давайте посмотрим на класс компонета и разберем его содержание. Полный текст класса выглядит так:

import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import org.primefaces.PrimeFaces; import org.primefaces.model.TreeNode;  import javax.annotation.ManagedBean; import javax.faces.component.UIComponent; import javax.faces.component.UIViewRoot; import javax.faces.context.FacesContext; import java.io.Serializable; import java.util.List;  @ManagedBean("treeContextMenuView") @ViewScoped public class PageMenuView implements Serializable {     private final TreeNode<Page> root;      private TreeNode<Page> selectedNode;     private final String DEFAULT_LIST = "employees.xhtml";     private String listSrc;      @Inject     public PageMenuView(PageService service) {         root = service.createPages();         listSrc = DEFAULT_LIST;     }       public TreeNode<Page> getRoot() {         return root;     }      public TreeNode<Page> getSelectedNode() {         return selectedNode;     }      public void setSelectedNode(TreeNode<Page> selectedNode) {         this.selectedNode = selectedNode;     }      public void setSrc() {         if (selectedNode.getData().getLink() != null) {             listSrc = selectedNode.getData().getLink();              UIViewRoot view = FacesContext.getCurrentInstance().getViewRoot();             List<UIComponent> uiComponents = view.getChildren();             UIComponent uiComponent = uiComponents.get(2).getChildren().get(0).getChildren().get(3).getChildren().get(1)                     .getChildren().get(0);             PrimeFaces.current().ajax().update(uiComponent.getClientId());         }     }      public String getSrc() {         return listSrc;     }  }  

Давайте сравним класс компонента с аналогичным классом, приведенным в документации Primefaces, чтобы понять, чем же он отличается. Оригинал файла можно найти здесь.

  1. в оригинале используется аннотация управляемого бина @Named("treeContextMenuView", у меня — @ManagedBean("treeContextMenuView"). Никакой разницы при использовании этих аннотаций я не обнаружил, кроме того, как я это делаю в других компонентах позже, можно также использовать спринговую аннотацию @Component("treeContextMenuView"). Вторая аннотация @ViewScoped из пакета jakarta.faces.view используется для того, чтобы создавался экземпляр бина, привязанный к сессии, создаваемой, когда вы открываете страницу index.xhtml. Подробности можно изучить здесь

  2. На этом сходство заканчивается. В оригинале сервисы инжектируются через поля, а первоначальное заполнение важного поля root, в котором хранится корень дерева узлов для меню, происходит в методе, помеченном аннотацией @PostConstruct. И это первое, что не будет у вас по умолчанию работать при интеграции Primefaces и Spring Boot проекта. Связано это с тем, что данная аннотация не работает со scoped бинами Spring. Я не пробовал делать проект на чистой Jakarta EE, но очевидно, что документация Primefaces ориентирована именно на такое базовое использование, вероятно, там это работать будет вполне нормально. У вас есть возможность попробовать это самостоятельно, если пожелаете изучить проблему подробнее. Разумеется, существуют способы заставить @PostConstruct работать так, как вам это нужно, и в scoped бинах. Например, могу привести статью, в которой достаточно подробно объясняется, почему такое поведение происходит в Spring, и как перенастроить инициализацию бина в контейнере, чтобы аннотация @PostConstruct отрабатывала. Но, как утверждал герой А. П. Чехова из рассказа «Письмо ученому соседу», «зачем на солнце пятны, когда и без них можно обойтиться». У нас Spring приложение + бины компонентов, управляющие загрузкой и исполнением xtml страницы. Возможностей вполне достаточно и без @PostConstruct. Поэтому все, что нужно выполнить при инициализации бина, я вынес в конструктор, а нужные в бине сервисы инжектировал также в конструкторе. Помимо этого, существует специальный способ выполнить какой-либо метод из бина компонента, указав его в метаданных страницы xhtml как метод, выполняемый всегда при загрузке страницы. Конкретно в этом компоненте я его не использую за ненадобностью, но позже, когда буду показывать вам работу некоторых других компонентов на других страницах, я к нему вернусь. Кроме того, я покажу в последующих частях статьи, как такой onload метод можно использовать и для совсем другой цели — для обновления отдельных компонентов при возвратах на текущую страницу из других страниц приложения.

Итак, у нас появился первый компонент на главное странице — меню приложения. Выглядит это примерно вот так:

Пункты для меню выбираются через сервисы и конкретно здесь для нас интереса не представляют, они зависят от бизнес-логики приложения, берутся из слоя сервисов, и у вас могут быть совершенно другими, под ваши нужды.

Но далее у нас будет еще одна задача — заставить меню по клику на отдельных пунктах обновить содержательную часть страницы данными из трех разных источников, которые будут выводиться в трех совершенно разных списках. Эта задача сама по себе не простая, и будет описана в следующей части статьи.

В конце статьи традиционно рекомендую посмотреть бесплатный урок от моих друзей из OTUS по теме: «Spring Data Projections, Example, Specifications».


ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/709726/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *