Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В предыдущей части мы разобрали, как заполнить инклуды контентом из дополнительных страниц, разместив на них компонент Primefaces Data Table Filter. Теперь в моем приложении по клику на пункте в левом меню в главной части страницы динамически подгружаются три разных списка, с разными источниками данных и разным набором колонок в таблицах. Но все они используют одну и ту же схему - компонент Data Table.
В таких таблицах хранят списки определенных сущностей - списки товаров, сотрудников, контрагентов и так далее. И очень часто бизнес-логика предполагает, что помимо списка должна существовать еще и карточка отдельной сущности, где можно было бы просмотреть или отредактировать ее данные.
Поскольку в моем приложении предполагается сравнительно простая ролевая модель безопасности, в которой просмотр всей карточки сущности и редактирование сущности разделяются по группам пользователей, я решил не усложнять разработку разделением настроек доступа и скрытием/отображением отдельных полей и компонентов на одной и той же странице, а просто сделать две отдельные страницы для двух отдельных карточек - карточки просмотра и карточки редактирования. Однако, так как данные, в основном, одни и те же, то управляемый бин с кодом компонентов я сделал один общий. Обратите внимание, что я написал "управляемый бин с кодом компонентов", а не "бин компонента", как было ранее в предыдущих частях статьи, потому что на самом деле в бине вполне можно размещать и настраивать код не одного, а нескольких компонентов, и даже из кода одного бина ссылаться на код других управляемых бинов. Впрочем, для тех, кто работал с фреймворком Spring, это привычная ситуация. Разница лишь в том, здесь я применяю инжектирования бина одного вида в бине другого вида, и таким образом, строго говоря, немного нарушаю классическую архитектуру MVC, превращая ее во что-то вроде M(VVV.....VVVV)C (цепочка вызываемых бинов вложенных видов может оказаться сколько угодно длинной, но обычно мне хватало двух, то есть я делал MVVC).
Начнем с карточки просмотра сущности. Вот пример одной из них, xhtml-страница:
<!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:p="http://primefaces.org/ui">
<f:metadata>
<f:viewParam name="id" value="#{employeeCardView.id}"/>
<f:viewAction action="#{employeeCardView.onload}"/>
<f:viewParam name="id" value="#{employeeFileDownloadView.id}"/>
<f:viewAction action="#{employeeFileDownloadView.onload}"/>
<f:viewParam name="id" value="#{employeeSkillsSelectionView.id}"/>
<f:viewAction action="#{employeeSkillsSelectionView.onload()}"/>
<f:viewParam name="id" value="#{employeeCardView.employeeRatingView.id}"/>
<f:viewAction action="#{employeeCardView.employeeRatingView.onload}"/>
</f:metadata>
<f:view contentType="text/html;charset=UTF-8" encoding="UTF-8">
<h:head>
<h:outputStylesheet library="webjars" name="primeflex/3.2.0/primeflex.min.css"/>
<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>
<div class="card">
<h:form>
<p:dataTable emptyMessage="">
<f:facet name="header">
<span>Карточка сотрудника</span>
</f:facet>
</p:dataTable>
</h:form>
<h:form id="form">
<div class="grid ui-fluid">
<div class="col-12 md:col-2">
<p:button href="/employee/edit/#{employeeCardView.id}" value="Редактировать"/>
</div>
<div class="col-12 md:col-2">
<p:button href="/" value="На главную"/>
</div>
</div>
<p:dialog modal="true" widgetVar="statusDialog" header="Status" draggable="false" closable="false"
resizable="false">
<i class="pi pi-spinner pi-spin" style="font-size:3rem"></i>
</p:dialog>
<h2>Общая информация</h2>
<p:divider/>
<div class="grid ui-fluid">
<div class="col-12 md:col-3">
<h3>Имя</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.firstName}"/>
<h3>Фамилия</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.surname}"/>
<h3>Отчество</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.secondName}"/>
</div>
<div class="col-12 md:col-3">
<h3>Подразделение</h3>
<h:outputText styleClass="fieldData"
value="#{employeeCardView.employeeDepartment.finDepartment.name}"/>
<h3>Возраст</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.age}"/>
<h3>Стаж работы</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.workExperience} лет"/>
</div>
<div class="col-12 md:col-3">
<h3>Ставка себестоимости</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.costPriceRate} в час"/>
<h3>Уволен/работает</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.archived}"/>
</div>
<div class="col-12 md:col-3">
<h3>Профиль роли сотрудника</h3>
<div class="col-12 md:col-12">
<h4>Основная роль</h4>
<h:outputText styleClass="fieldData mb-4" value="#{employeeCardView.employeeRatingView.mainRoleName}"/>
<p:rating id="rate_1" value="#{employeeCardView.employeeRatingView.mainGradeId}" readonly="false" stars="6"/>
</div>
<div class="col-12 md:col-12">
<h4>Дополнительные роли</h4>
<p:dataList value="#{employeeCardView.employeeRatingView.extraRoleEntryList}"
var="entry" styleClass="noBorders" itemType="none">
<p>
#{entry.key.name}
</p>
<p:rating value="#{entry.value.id}" readonly="false" stars="6"/>
</p:dataList>
</div>
<div class="col-12 md:col-8">
<p:button value="Редактировать" icon="pi pi-pencil"
href="/employee/roles/edit/#{employeeCardView.id}"/>
</div>
</div>
<p:divider/>
<div class="col-12 md:col-3">
<h3>Локация</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.location.city}"/>
, <h:outputText styleClass="fieldData" value="#{employeeCardView.location.personalAddress}"/>
<h3>Контактные данные</h3>
<div><h:outputText styleClass="fieldData" value="Email: #{employeeCardView.contactObj.email}"/>
</div>
<div><h:outputText styleClass="fieldData" value="Phone: #{employeeCardView.contactObj.phone}"/>
</div>
<div><h:outputText styleClass="fieldData"
value="Mobile: #{employeeCardView.contactObj.mobile}"/>
</div>
</div>
<div class="col-12 md:col-3">
<h3>Образование</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.education.organization}"/>,
<h:outputText styleClass="fieldData" value="#{employeeCardView.education.educationGrade.name}"/>
<h3>Грейд</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.grade.name}"/>
<h3>Уровень владения английским языком</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.englishLevel.name}"/>
</div>
<div class="col-12 md:col-3">
<h3>Резюме на русском языке</h3>
<h:outputText value="будет сгенерировано автоматически"/>
<h:outputText styleClass="fieldData" value="#{employeeCardView.russianResume}"/>
<h3>Резюме на английском языке</h3>
<h:outputText value="будет сгенерировано автоматически"/>
<h:outputText styleClass="fieldData" value="#{employeeCardView.englishResume}"/>
</div>
</div>
<p:divider/>
<h2>Дополнительная информация</h2>
<p:divider/>
<div class="grid ui-fluid">
<div class="col-12 md:col-4">
<h3>Квалификационный профиль сотрудника</h3>
<p:dataList value="#{employeeCardView.skillNamesSet}"
var="level" styleClass="noBorders" itemType="none">
<p>
#{level}
</p>
</p:dataList>
<div class="col-12 md:col-4">
<p:commandButton icon="pi pi-window-maximize"
oncomplete="PF('detailDialog').show()"
value="Подробности"/>
</div>
</div>
<div class="col-12 md:col-4">
<h3>Дополнительная информация от сотрудника</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.selfInfo}"/>
</div>
<div class="col-12 md:col-4">
<h3>Дополнительная информация от руководителя</h3>
<h:outputText styleClass="fieldData" value="#{employeeCardView.chefInfo}"/>
</div>
</div>
<p:divider/>
<div class="grid ui-fluid">
<div class="col-12 md:col-12">
<h3>Проектный опыт</h3>
<h4>будет выгружаться из 1С</h4>
<p:dataTable lazy="false" var="item" value="#{employeeCardView.satelProjectExperienceList}"
widgetVar="itemsTable"
emptyMessage="Проектный опыт у сотрудника отсутствует">
<p:column headerText="№ сделки">
<h:outputText value="#{item.contractNumber}"/>
</p:column>
<p:column headerText="Название проекта">
<h:outputText value="#{item.name}"/>
</p:column>
<p:column headerText="Проектная роль">
<h:outputText value="#{item.projectRole} часов"/>
</p:column>
<p:column headerText="Проектные работы">
<h:outputText value="#{item.projectWorks}"/>
</p:column>
</p:dataTable>
</div>
</div>
<div class="grid ui-fluid">
<div class="col-12 md:col-6">
<h3>Файлы</h3>
<p:dataList id="employee-files-list" lazy="false" var="entry" widgetVar="itemsTable"
itemStyleClass="fileItem"
emptyMessage="Файлы сотрудника не загружены" itemType="none"
value="#{employeeCardView.employeeFileDownloadView.streamedContentEntrySet}">
<span class="fileButton">
<p:commandButton value="#{entry.value.name}" ajax="false"
onclick="PrimeFaces.monitorDownload(start, stop);"
icon="pi pi-arrow-down" styleClass="mr-2">
<p:fileDownload value="#{entry.value.streamedContent}"/>
</p:commandButton>
</span>
</p:dataList>
</div>
</div>
<div class="grid ui-fluid">
<div class="col-12 md:col-2">
<p:button href="/employee/edit/#{employeeCardView.id}" value="Редактировать"/>
</div>
<div class="col-12 md:col-2">
<p:button href="/" value="На главную"/>
</div>
</div>
</h:form>
<h:form id="dialogs">
<p:dialog header="Компетенции сотрудника" showEffect="fade" modal="true"
widgetVar="detailDialog"
responsive="true">
<p:outputPanel id="detail-skill-content" class="ui-fluid">
<p:dataList value="#{employeeCardView.skillNamesSet}"
var="level" styleClass="noBorders" itemType="none">
<p>
#{level}
</p>
<div>
<p:rating value="3" readonly="false" stars="6"/>
</div>
<p><h:outputText>Здесь будут подробности оценки навыка</h:outputText></p>
<p:divider/>
</p:dataList>
</p:outputPanel>
<f:facet name="footer">
<p:button value="Редактировать" icon="pi pi-pencil"
href="/employee/skills/edit/#{employeeCardView.id}"/>
<p:commandButton value="Закрыть" icon="pi pi-times" onclick="PF('detailDialog').hide()"
class="ui-button-secondary" type="button"/>
</f:facet>
</p:dialog>
</h:form>
</div>
</h:body>
</f:view>
</html>
Разберем чуть подробнее некоторые поля (многие поля имеют одинаковые типы, поэтому нет смысла описывать их все)
Самое простое поле, которое я уже упоминал ранее, это h:outputText
. Все, что нужно для его базового использования, это указать ссылку на поле в бине, к которому оно будет привязано, например:
<h:outputText styleClass="fieldData" value="#{employeeCardView.firstName}"/>
здесь поле будет привязано к полю firstName в бине с именем employeeCardView. Естественно, в бине это поле имеет какой-то из примитивных типов. Для вывода данных этого вполне достаточно. В случаях, если данные для вывода должны получаться из каких-то отдельных источников или генерироваться из других данных, ничто не мешает нам сделать это в самом бине через обращение к сервисам или написав непосредственно метод для генерации данных из нескольких полей или из источника, а затем записать полученный результат в новое отдельное поле бина, которое также будет привязано к полю xhtml файла.
Иногда бывает необходимо вывести некоторый список данных, которые хранятся в каком-то поле бина, имеющем тип одной из коллекций Java. В этом случае удобно использовать, например, компонент Data List, например:
<p:dataList value="#{employeeCardView.employeeRatingView.extraRoleEntryList}"
var="entry" styleClass="noBorders" itemType="none">
<p>
#{entry.key.name}
</p>
<p:rating value="#{entry.value.id}" readonly="false" stars="6"/>
</p:dataList>
причем передаваемая коллекция может быть достаточно сложной. В этом примере в параметр компонента value передается ссылка на список элементов Map.Entry, извлеченных в компоненте из данных некоторой Map, привязанное поле бина выглядит следующим образом:
private List<Map.Entry<Role, Grade>> extraRoleEntryList;
параметр var компонета служит для передачи в список значения отдельной Map.Entry<Role, Grade> из списка, соответственно, мы видим, что в списке будет выводиться имя ключа, то есть поле name экземпляра класса Role.
Пожалуй, вывод данных из мапы - это самый сложный вариант вывода данных из коллекций, из простых списков по аналогии получить вывод намного легче, читатель легко с этим разберется самостоятельно. Но на всякий случай приведу ссылку на то, как это делается, в документции Primefaces:
http://www.primefaces.org:8080/showcase/ui/data/datalist/basic.xhtml?jfwid=513c8
Необязательно, чтобы поле было списком, оно может быть, например, и множеством:
<p:dataList value="#{employeeCardView.skillNamesSet}"
var="level" styleClass="noBorders" itemType="none">
<p>
#{level}
</p>
</p:dataList>
private Set<String> skillNamesSet;
В следующей части статьи мы продолжим разбор еще нескольких интересных способов вывода информации в разных типах полей для просмотра.
Традиционно в конце статьи рекомендую вам бесплатный урок от OTUS. В этот раз хочу порекомендовать урок, в рамках которого будет рассмотрена общая архитектура веб-приложений. Что происходит при клиент-серверном взаимодействии. И какое место отведено Spring MVC.
Подробнее об открытом уроке