6. 示例应用程序-03:rdvmedecins-pf-ejb
让我们回顾一下为 GlassFish 服务器开发的示例应用程序的结构:
![]() |
除了 Web 层之外,我们不会对该架构进行任何更改,该层将在此使用 JSF 和 PrimeFaces 进行实现。
![]() |
6.1. NetBeans 项目
上文中的 [business] 和 [DAO] 层来自示例 01 JSF / EJB / Glassfish。我们将复用它们。
![]() |
- [mv-rdvmedecins-ejb-dao-jpa]: 示例 01 中 [DAO] 和 [JPA] 层的 EJB 项目,
- [mv-rdvmedecins-ejb-metier]: 示例 01 中 [业务] 层的 EJB 项目,
- [mv-rdvmedecins-pf]:[Web] 层项目 / Primefaces – 新增,
- [mv-rdvmedecins-app-ear]:用于在 GlassFish 服务器上部署应用程序的企业级项目 – 新增。
6.2. 该企业项目
该企业项目仅用于将三个模块 [mv-rdvmedecins-ejb-dao-jpa]、[mv-rdvmedecins-ejb-business]、[mv-rdvmedecins-pf] 部署到 GlassFish 服务器上。NetBeans 项目结构如下:
![]() |
该项目仅用于管理[pom.xml]文件中定义的以下三个依赖项[1]:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>mv-rdvmedecins-app</artifactId>
<groupId>istia.st</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>istia.st</groupId>
<artifactId>mv-rdvmedecins-app-ear</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>ear</packaging>
<name>mv-rdvmedecins-app-ear</name>
...
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>${project.version}</version>
<type>ejb</type>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-metier</artifactId>
<version>${project.version}</version>
<type>ejb</type>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-pf</artifactId>
<version>${project.version}</version>
<type>war</type>
</dependency>
</dependencies>
</project>
- 第 10–13 行:企业项目的 Maven 工件,
- 第 18–37 行:该项目的三个依赖项。请注意它们的类型(第 23、29、35 行)。
要运行 Web 应用程序,必须运行此企业项目。
6.3. PrimeFaces Web 项目
PrimeFaces Web 项目如下:
![]() |
- 在 [1] 中,列出了该项目的页面。其中 [index.xhtml] 页面是该项目唯一的页面。它包含三个片段:[form1.xhtml]、[form2.xhtml] 和 [error.xhtml]。其余页面仅用于格式示例。
- 在 [2] 中,是 Java Bean。其中 [Application] Bean 具有应用程序作用域,而 [Form] Bean 具有会话作用域。[Error] 类封装了错误信息。[MyDataModel] 类作为 PrimeFaces <dataTable> 标签的模型,
- 在 [3] 中,是用于国际化的消息文件,
- 在 [4] 中,是依赖项。Web 项目依赖于 EJB 项目中的 [DAO] 层、EJB 项目中的 [business] 层,以及 PrimeFaces 中的 [web] 层。
6.4. 项目配置
该项目的配置与我们之前学习的 PrimeFaces 或 JSF 项目相同。我们在此列出配置文件,不再赘述。
![]() | ![]() |
[web.xml]:用于配置 Web 应用程序。
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Production</param-value>
</context-param>
<context-param>
<param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
<error-page>
<error-code>500</error-code>
<location>/faces/exception.xhtml</location>
</error-page>
<error-page>
<exception-type>Exception</exception-type>
<location>/faces/exception.xhtml</location>
</error-page>
</web-app>
请注意,第 30 行中的 [index.xhtml] 页面是应用程序的首页。
[faces-config.xml]:配置 JSF 应用程序
<?xml version='1.0' encoding='UTF-8'?>
<!-- =========== FULL CONFIGURATION FILE ================================== -->
<faces-config version="2.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">
<application>
<resource-bundle>
<base-name>
messages
</base-name>
<var>msg</var>
</resource-bundle>
<message-bundle>messages</message-bundle>
</application>
</faces-config>
[beans.xml]:内容为空,但 @Named 注解需要该文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>
[styles.css]:应用程序的样式表
.col1{
background-color: #ccccff
}
.col2{
background-color: #ffcccc
}
PrimeFaces 库自带了专属的样式表。上方的样式表仅用于在发生异常时显示的页面——该页面不受应用程序管理。此时将显示 [exception.xhtml] 页面。
[messages_fr.properties]:法语消息文件
# layout
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers - application propuls\u00e9e par PrimeFaces et JQuery
# exception
exception.header=L'exception suivante s'est produite
exception.httpCode=Code HTTP de l'erreur
exception.message=Message de l'exception
exception.requestUri=Url demand\u00e9e lors de l'erreur
exception.servletName=Nom de la servlet demand\u00e9e lorsque l'erreur s'est produite
# formulaire 1
form1.titre=R\u00e9servations
form1.medecin=M\u00e9decin
form1.jour=Jour
form1.options=Options
form1.francais=Fran\u00e7ais
form1.anglais=Anglais
form1.rafraichir=Rafra\u00eechir
form1.precedent=Jour pr\u00e9c\u00e9dent
form1.suivant=Jour suivant
form1.agenda=Affiche l'agenda du m\u00e9decin choisi pour le jour choisi
form1.today=Aujourd'hui
# formulaire 2
form2.titre=Agenda de {0} {1} {2} le {3}
form2.titre_detail=Agenda de {0} {1} {2} le {3}
form2.creneauHoraire=Cr\u00e9neau horaire
form2.client=Client
form2.accueil=Accueil
form2.supprimer=Supprimer
form2.reserver=R\u00e9server
form2.valider=Valider
form2.annuler=Annuler
form2.erreur=Erreur
form2.emtyMessage=Pas de cr\u00e9neaux entr\u00e9s dans la base
form2.suppression.confirmation=Etes-vous s\u00fbr(e) ?
form2.suppression.message=Suppression d'un rendez-vous
form2.supprimer.oui=Oui
form2.supprimer.non=Non
form2.erreurClient=Client [{0}] inconnu
form2.erreurClient_detail=Client {0} inconnu
form2.erreurAction=Action non autoris\u00e9e
form2.erreurAction_detail=Action non autoris\u00e9e
# erreur
erreur.titre=Une erreur s'est produite.
erreur.exceptions=Cha\u00eene des exceptions
erreur.type=Type de l'exception
erreur.message=Message associ\u00e9
erreur.accueil=Accueil
[messages_en.properties]:英文消息文件
# layout
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university - Application powered by PrimeFaces and JQuery
# exception
exception.header=The following exceptions occurred
exception.httpCode=Error HTTP code
exception.message=Exception message
exception.requestUri=Url targeted when error occurred
exception.servletName=Servlet targeted's name when error occurred
# formulaire 1
form1.titre=Reservations
form1.medecin=Doctor
form1.jour=Date
form1.options=Options
form1.francais=French
form1.anglais=English
form1.rafraichir=Refresh
form1.precedent=Previous Day
form1.suivant=Next day
form1.agenda=Show the doctor's diary for the chosen doctor and the chosen day
form1.today=Today
# formulaire 2
form2.titre={0} {1} {2}'' diary on {3}
form2.titre_detail={0} {1} {2}'' diary on {3}
form2.creneauHoraire=Time Period
form2.client=Client
form2.accueil=Welcome Page
form2.supprimer=Delete
form2.reserver=Reserve
form2.valider=Submit
form2.annuler=Cancel
form2.erreur=Error
form2.emtyMessage=No Time periods in the database
form2.suppression.confirmation=Are-you sure ?
form2.suppression.message=Booking deletion
form2.supprimer.oui=Yes
form2.supprimer.non=No
form2.erreurClient=Unknown Client {0}
form2.erreurClient_detail=Unknown Client [{0}]
form2.erreurAction=Unauthorized action
form2.erreurAction_detail=Action non autoris\u00e9e
# erreur
erreur.titre=The following exceptions occurred
erreur.exceptions=Exceptions' chain
erreur.type=Exception type
erreur.message=Associated Message
erreur.accueil=Welcome
6.5. 页面模板 [layout.xhtml]
![]() |
[layout.xhtml] 模板如下:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{form.locale}">
<h:head>
<title>JSF</title>
<h:outputStylesheet library="css" name="styles.css"/>
</h:head>
<h:body style="background-image: url('#{request.contextPath}/resources/images/standard.jpg');">
<h:form id="formulaire">
<table style="width: 1200px">
<tr>
<td colspan="2" bgcolor="#ccccff">
<ui:include src="entete.xhtml"/>
</td>
</tr>
<tr>
<td style="width: 10px;" bgcolor="#ffcccc">
<ui:include src="menu.xhtml"/>
</td>
<td>
<p:outputPanel id="contenu">
<ui:insert name="contenu">
<h2>Contenu</h2>
</ui:insert>
</p:outputPanel>
</td>
</tr>
<tr bgcolor="#ffcc66">
<td colspan="2">
<ui:include src="basdepage.xhtml"/>
</td>
</tr>
</table>
</h:form>
</h:body>
</f:view>
</html>
此模板中唯一可变的部分是第28至30行处的区域。该区域位于 :form:content ID 中(第27行)。请牢记这一点。用于更新该区域的AJAX调用将带有 update=":form:content" 属性。此外,表单从第15行开始。因此,插入在第28至30行处的片段将被插入到该表单中。
此模板生成以下输出:
![]() |
页面的动态部分将被插入到上方的框选区域中。
6.6. [index.xhtml] 页面
![]() |
该项目始终显示同一页面,即以下 [index.xhtml] 页面:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="layout.xhtml">
<ui:define name="contenu">
<ui:fragment rendered="#{form.form1Rendered}">
<ui:include src="form1.xhtml"/>
</ui:fragment>
<ui:fragment rendered="#{form.form2Rendered}">
<ui:include src="form2.xhtml"/>
</ui:fragment>
<ui:fragment rendered="#{form.erreurRendered}">
<ui:include src="erreur.xhtml"/>
</ui:fragment>
</ui:define>
</ui:composition>
</html>
- 第 8-9 行:此 XHTML 片段将被插入到 [layout.xhtml] 模板的动态区域中,
- 该页面由三个子片段组成:
- [form1.xhtml],第10–12行;
- [form2.xhtml],第 13–15 行;
- [error.xhtml],第 16–18 行。
这些片段在 [index.xhtml] 中的显示由与该页面关联的 [Form.java] 模板中的布尔值控制。因此,通过调整这些布尔值,渲染出的页面会发生变化。
[form1.xhtml] 片段的渲染效果如下:
[form2.xhtml] 片段的渲染效果如下:
![]() |
[erreur.xhtml] 片段的渲染效果如下:
![]() |
6.7. 该项目的 Bean
![]() |
[utils] 包中的类已作介绍:[Messages] 类有助于实现应用程序消息的国际化。相关内容已在第 2.8.5.7 节中讨论过。
6.7.1. 应用程序 Bean
[Application.java] Bean 是一个应用程序范围的 Bean。回顾一下,此类 Bean 用于存储可供应用程序所有用户访问的只读数据。该 Bean 如下所示:
package beans;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import rdvmedecins.metier.service.IMetierLocal;
@Named(value = "application")
@ApplicationScoped
public class Application {
// business layer
@EJB
private IMetierLocal metier;
public Application() {
}
// getters
public IMetierLocal getMetier() {
return metier;
}
}
- 第 8 行:我们将 Bean 的名称设为 "application",
- 第 9 行:它具有应用程序作用域,
- 第13–14行:应用服务器的EJB容器将向其中注入对[业务]层本地接口的引用。让我们回顾一下应用程序架构:
![]() |
JSF 应用程序和 [Business] EJB 将运行在同一个 JVM(Java 虚拟机)中。因此,[JSF] 层将使用 EJB 的本地接口。仅此而已。[Application] Bean 中不包含其他内容。其他 Bean 若要访问 [Business] 层,将从该 Bean 中获取它。
6.7.2. [Error] Bean
[Error] 类如下所示:
- package beans;
public class Erreur {
public Erreur() {
}
// field
private String classe;
private String message;
// manufacturer
public Erreur(String classe, String message){
this.setClasse(classe);
this.message=message;
}
// getters and setters
...
}
- 第 9 行:如果抛出了异常,则显示异常类的名称,
- 第 10 行:错误消息。
6.7.3. [Form] Bean
其代码如下:
package beans;
import java.io.IOException;
...
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// bean Application
@Inject
private Application application;
// session cache
private List<Medecin> medecins;
private List<Client> clients;
private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
private Map<Long, Client> hClients = new HashMap<Long, Client>();
private Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
// model
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneauChoisi;
private Medecin medecin;
private Long idClient;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
private Boolean erreur = false;
private String identiteClient;
private String action;
private String msgErreurClient;
private Boolean erreurClient;
private String msgErreurAction;
private Boolean erreurAction;
private String locale = "fr";
@PostConstruct
private void init() {
// caching doctors and customers
try {
medecins = application.getMetier().getAllMedecins();
clients = application.getMetier().getAllClients();
} catch (Throwable th) {
// we note the error
prepareVueErreur(th);
return;
}
// dictionaries
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
hIdentitesClients.put(identite(c), c);
}
}
...
// view display
private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean erreurRendered) {
this.form1Rendered = form1Rendered;
this.form2Rendered = form2Rendered;
this.erreurRendered = erreurRendered;
}
// preparation vueErreur
private void prepareVueErreur(Throwable th) {
// create an error list
erreurs = new ArrayList<Erreur>();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
while (th.getCause() != null) {
th = th.getCause();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
}
// the error view is displayed
setForms(true, false, true);
}
// getters and setters
...
}
- 第 6-8 行:[Form] 类是一个名为“form”且作用域为会话的 Bean。请注意,因此该类必须是可序列化的,
- 第 14–15 行:表单 Bean 持有对应用程序 Bean 的引用。该引用将由应用程序运行的 Servlet 容器注入(@Inject 注解的存在)。
- 第 17–44 行:页面模板 [form1.xhtml, form2.xhtml, error.xhtml]。这些页面的显示由第 27–29 行的布尔变量控制。请注意,默认情况下渲染 [form1.xhtml] 页面(第 27 行),
- 第 46–47 行:类实例化后立即执行 `init` 方法(因存在 `@PostConstruct` 注解),
- 第 50–51 行:向 [business] 层查询医生和客户的列表,
- 第 59–65 行:如果一切顺利,将构建医生和客户的字典。它们按编号进行索引。接下来,将显示 [form1.xhtml] 页面(第 27 行),
- 第 54 行:若发生错误,则构建 [error.xhtml] 页面模板。该模板包含第 36 行输出的错误列表,
- 第 78–88 行:[prepareVueErreur] 方法构建待显示的错误列表。随后 [index.xhtml] 页面将显示 [form1.xhtml] 和 [erreur.xhtml] 片段(第 87 行)。
[error.xhtml] 页面内容如下:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<p:panel header="#{msg['erreur.titre']}" closable="true" >
<hr/>
<p:dataTable value="#{form.erreurs}" var="erreur">
<f:facet name="header">
<h:outputText value="#{msg['erreur.exceptions']}"/>
</f:facet>
<p:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.type']}"/>
</f:facet>
<h:outputText value="#{erreur.classe}"/>
</p:column>
<p:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.message']}"/>
</f:facet>
<h:outputText value="#{erreur.message}"/>
</p:column>
</p:dataTable>
</p:panel>
</body>
</html>
它使用 <p:dataTable> 标签(第 12–28 行)来显示错误列表。这将生成一个类似于以下内容的错误页面:
![]() |
接下来我们将定义应用程序生命周期的各个阶段。针对每项用户操作,我们将分析相关的视图和事件处理程序。
6.8. 显示主页
如果一切正常,首先显示的页面是 [form1.xhtml]。这将呈现以下视图:
![]() |
[form1.xhtml] 页面内容如下:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<p:toolbar>
<p:toolbarGroup align="left">
...
</p:toolbarGroup>
<p:toolbarGroup align="right">
...
</p:toolbarGroup>
</p:toolbar>
</html>
截图中突出显示的工具栏是 PrimeFaces 工具栏组件。它在第 8–14 行中定义。它包含两组组件,每组由一个 <toolbarGroup> 标签定义,分别位于第 9–11 行和第 12–14 行。其中一组对齐在工具栏左侧(第 9 行),另一组对齐在右侧(第 12 行)。
让我们来看看左侧组中的几个组件:
<p:toolbar>
<p:toolbarGroup align="left">
<h:outputText value="#{msg['form1.medecin']}"/>
<p:selectOneMenu value="#{form.idMedecin}" effect="fade">
<f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>
<p:ajax update=":formulaire:contenu" listener="#{form.hideAgenda}" />
</p:selectOneMenu>
<p:separator/>
<h:outputText value="#{msg['form1.jour']}"/>
<p:calendar id="calendrier" value="#{form.jour}" readOnlyInputText="true">
<p:ajax event="dateSelect" listener="#{form.hideAgenda}" update=":formulaire:contenu"/>
</p:calendar>
<p:separator/>
<p:commandButton id="resa-agenda" icon="ui-icon-check" actionListener="#{form.getAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-agenda" value="#{msg['form1.agenda']}"/>
...
</p:toolbarGroup>
...
- 第4-7行:已添加效果(effect="fade")的医生下拉菜单,
- 第 6 行:一个 AJAX 行为。当下拉列表发生变化时,将执行 [Form].hideAgenda 方法(listener="#{form.hideAgenda}"),并更新动态区域 :form:content(update=":form:content"),
- 第 8 行:在工具栏中添加分隔符,
- 第 10–12 行:日期输入字段。此处使用了 PrimeFaces 日历。该输入字段为只读(readOnlyInputText="true"),
- 第 11 行:一个 AJAX 行为。当日期发生变化时,将执行 [Form].hideAgenda 方法,并更新动态字段 :form:content,
- 第 14 行:一个按钮。点击它将触发对 [Form].getAgenda() 方法的 AJAX 调用;随后模型将被修改,并使用服务器响应更新动态 :form:content 字段,
- 第 15 行:<tooltip> 标签允许您为组件关联一个工具提示。组件的 ID 由工具提示的 for 属性指定。此处 (for="resa-agenda") 指代第 14 行的按钮:
![]() | ![]() |
本页面由以下模板驱动:
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// session cache
private List<Medecin> medecins;
private List<Client> clients;
// model
private Long idMedecin;
private Date jour = new Date();
// list of doctors
public List<Medecin> getMedecins() {
return medecins;
}
// customer list
public List<Client> getClients() {
return clients;
}
// agenda
public void getAgenda() {
...
}
- 第 12 行中的字段会读取并写入页面第 4 行列表的值。页面首次加载时,它会设置下拉列表中选中的值。在初始加载时,idMedecin 等于 null,因此将选中第一位医生。
- 第 16–18 行中的方法用于生成“医生”下拉列表(页面第 5 行)中的选项。每个生成的选项将使用医生的头衔、姓氏和名字作为标签(itemLabel),并使用医生的 ID 作为值(itemValue),
- 第13行的字段为页面第10行的输入字段提供了读写访问权限。初始显示时,会显示当前日期,
- 第26–28行:getAgenda方法处理页面第14行[Agenda]按钮的点击事件。它与JSF版本中的实现几乎完全相同:
// bean Application
@Inject
private Application application;
// session cache
private List<Medecin> medecins;
private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
// model
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneauChoisi;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
private Boolean erreur = false;
public void getAgenda() {
try {
// we get the doctor back
medecin = hMedecins.get(idMedecin);
// the doctor's diary for a given day
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// form 2 is displayed
setForms(true, true, false);
} catch (Throwable th) {
// error view
prepareVueErreur(th);
}
// no slots selected yet
creneauChoisi = null;
}
我们不会对这段代码进行评论。这已经有人做过了。
6.9. 显示医生的日程安排
6.9.1. 日程安排概览
以下是该用例:
![]() |
- 在[1]中,您选择一位医生[1]和一天[2],然后请求[3]该医生在所选日期的日程安排;
- 在[4]中,它会显示在工具栏下方。
页面 [form2.xhtml] 的代码如下:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jsp/jstl/core">
<body>
<!-- context menu -->
<p:contextMenu for="agenda">
...
</p:contextMenu>
<!-- agenda -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- schedule column -->
<p:column style="width: 100px">
...
</p:column>
<!-- customer column -->
<p:column style="width: 300px">
...
</p:column>
</p:dataTable>
<!-- confirmation deletion RV -->
<p:confirmDialog id="confirmDialog" message="#{msg['form2.suppression.confirmation']}"
header="#{msg['form2.suppression.message']}" severity="alert" widgetVar="confirmation">
...
</p:confirmDialog>
<!-- error message -->
<p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >
...
</p:dialog>
<!-- server return management -->
<script type="text/javascript">
...
}
</script>
</body>
</html>
- 第16-26行:页面主要元素是显示医生日程的<dataTable>,
![]() |
- 第12-14行:我们将使用上下文菜单来添加/删除预约:
![]() |
- 第 29-32 行:当用户想要删除一个约会时,将显示一个确认对话框:
![]() |
- 第35至37行:将使用对话框来报告错误:
![]() |
- 第 40–43 行:我们需要添加一些 JavaScript 代码。
6.9.2. 预约表
在此我们将介绍第5.15节(第327页)中讨论的数据表模型。
让我们来看看该页面的主要元素——显示日历的表格:
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- schedule column -->
<p:column style="width: 100px">
...
</p:column>
<!-- customer column -->
<p:column style="width: 300px">
...
</p:column>
</p:dataTable>
结果如下:
![]() |
这是一个两列表格(第 4–6 行和第 8–10 行),数据由源 [Form].getMyDataModel() (value="#{form.myDataModel}") 填充。 每次只能选择一行(selectionMode="single")。每次POST请求时,选中的项的引用会被赋值给 [Form].creneauChoisi(selection="#{form.creneauChoisi}")。
请注意,getAgenda 方法在模型中初始化了以下字段:
// modèle
private AgendaMedecinJour agendaMedecinJour;
该数据表模型是通过调用以下 [Form].getMyDataModel 方法(即 <dataTable> 标签的 value 属性)获取的:
// the dataTable model
public MyDataModel getMyDataModel() {
return new MyDataModel(agendaMedecinJour.getCreneauxMedecinJour());
}
让我们来分析一下 [MyDataModel] 类,它作为 <p:dataTable> 标签的模型:
package beans;
import javax.faces.model.ArrayDataModel;
import org.primefaces.model.SelectableDataModel;
import rdvmedecins.metier.entites.CreneauMedecinJour;
public class MyDataModel extends ArrayDataModel<CreneauMedecinJour> implements SelectableDataModel<CreneauMedecinJour> {
// manufacturers
public MyDataModel() {
}
public MyDataModel(CreneauMedecinJour[] creneauxMedecinJour) {
super(creneauxMedecinJour);
}
@Override
public Object getRowKey(CreneauMedecinJour creneauMedecinJour) {
return creneauMedecinJour.getCreneau().getId();
}
@Override
public CreneauMedecinJour getRowData(String rowKey) {
// list of slots
CreneauMedecinJour[] creneauxMedecinJour = (CreneauMedecinJour[]) getWrappedData();
// the key is a long integer
long key = Long.parseLong(rowKey);
// search for the selected slot
for (CreneauMedecinJour creneauMedecinJour : creneauxMedecinJour) {
if (creneauMedecinJour.getCreneau().getId().longValue() == key) {
return creneauMedecinJour;
}
}
// nothing
return null;
}
}
- 第 7 行:[MyDataModel] 类是 <p:dataTable> 标签的模型。该类的目的是将提交的 rowkey 元素与该行关联的元素进行关联,
- 第 7 行:该类通过 [ArrayDataModel] 类实现了 [SelectableDataModel] 接口。这意味着构造函数的参数是一个数组。该数组用于填充 <dataTable> 标签。在此,数组的每一行都将与一个类型为 [CreneauMedecinJour] 的元素相关联,
- 第 13–15 行:构造函数将其参数传递给父类,
- 第 18–20 行:数组的每一行对应一个时间段,并通过该时间段的 ID 进行标识(第 19 行)。正是这个 ID 会被提交至服务器,
- 第 23 行:当发布时间段 ID 时,将在服务器端执行的代码。此方法的目的是返回与该 ID 关联的 [CreneauMedecinJour] 对象的引用。该引用将被赋值给 <dataTable> 标签的 selection 属性的 target 属性:
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
因此,[Form].creneauChoisi 字段将包含您要添加或删除的 [CreneauMedecinJour] 对象的引用。
6.9.3. 时段列
![]() |
时间段列是通过以下代码生成的:
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- schedule column -->
<p:column style="width: 100px">
<f:facet name="header">
<h:outputText value="#{msg['form2.creneauHoraire']}"/>
</f:facet>
<div align="center">
<h:outputFormat value="{0,number,#00}:{1,number,#00} - {2,number,#00}:{3,number,#00}">
<f:param value="#{creneauMedecinJour.creneau.hdebut}" />
<f:param value="#{creneauMedecinJour.creneau.mdebut}" />
<f:param value="#{creneauMedecinJour.creneau.hfin}" />
<f:param value="#{creneauMedecinJour.creneau.mfin}" />
</h:outputFormat>
</div>
</p:column>
<!-- customer column -->
<p:column style="width: 300px">
...
</p:column>
</p:dataTable>
- 第 5–7 行:列标题,
- 第 8–15 行:当前列元素。请注意第 9 行,其中使用 <h:outputFormat> 标签来格式化待显示的元素。value 参数指定要显示的字符串。 表示法 {i,type,format} 分别指第 i 个参数、该参数的类型及其格式。此处共有 4 个参数,编号为 0 至 3;其类型为数值型,且将以两位数形式显示,
- 第 10–13 行:<h:outputFormat> 标签所期望的四个参数。
6.9.4. 客户列
![]() |
“客户”列是通过以下代码生成的:
<!-- agenda -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- schedule column -->
...
<!-- customer column -->
<p:column style="width: 300px">
<f:facet name="header">
<h:outputText value="#{msg['form2.client']}"/>
</f:facet>
<ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
<h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}" />
</ui:fragment>
<ui:fragment rendered="#{creneauMedecinJour.rv==null and form.creneauChoisi!=null and form.creneauChoisi.creneau.id==creneauMedecinJour.creneau.id}">
...
</ui:fragment>
</p:column>
</p:dataTable>
- 第 8–10 行:列标题,
- 第 11–13 行:当时间段内有预约时,显示当前元素。在此情况下,我们会显示该预约所对应的客户的标题、名字和姓氏,
- 第14–16行:另一个片段,我们稍后会再回来处理。
6.10. 删除预约
删除预约涉及以下步骤:
![]() |
![]() |
本案涉及的观点如下:
<!-- contextual menu -->
<p:contextMenu for="agenda">
...
<p:menuitem value="#{msg['form2.supprimer']}" onclick="confirmation.show()"/>
</p:contextMenu>
<!-- agenda -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
...
</p:dataTable>
<!-- confirm deletion RV -->
<p:confirmDialog id="confirmDialog" message="#{msg['form2.suppression.confirmation']}"
header="#{msg['form2.suppression.message']}" severity="alert" widgetVar="confirmation">
<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
oncomplete="handleRequest(xhr, status, args); confirmation.hide()">
<f:setPropertyActionListener value="supprimer" target="#{form.action}"/>
</p:commandButton>
<p:commandButton value="#{msg['form2.supprimer.non']}" onclick="confirmation.hide()" type="button" />
</p:confirmDialog>
- 第2-5行:一个与数据数组(用于属性)关联的上下文菜单。它包含两个选项 [1]:
![]() |
- 第 4 行:[删除] 选项会触发显示第 13-20 行中的 [2] 对话框,
- 第15行:点击[是]将触发[Form.action]的执行,从而删除该预约。通常情况下,如果所选项目没有预约,右键菜单不应提供[删除]选项;如果所选项目已有预约,则不应提供[预订]选项。 我们无法让右键菜单做到如此精细。虽然对第一个选中的项目有效,但随后我们发现右键菜单会保留针对该首次选择设置的配置,导致后续操作出现错误。因此我们保留了这两个选项,并决定在用户删除没有预约的项目时向其提供提示,
- 第 16 行:`oncomplete` 属性,允许您定义在 AJAX 请求完成后执行的 JavaScript 代码。在此情况下,代码如下:
<!-- message d'erreur -->
<p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >
<h:outputText value="#{form.msgErreur}" />
</p:dialog>
<!-- gestion du retour serveur -->
<script type="text/javascript">
function handleRequest(xhr, status, args) {
// erreur ?
if(args.erreur) {
dlgErreur.show();
}
}
</script>
- 第 10 行:JavaScript 代码检查 args 字典是否具有 error 属性。如果有,则显示第 2 行中的对话框(widgetVar 属性)。该对话框显示 [Form].msgError 模板。
让我们看看处理删除预约时执行的代码:
<p:confirmDialog ...>
<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
...>
<f:setPropertyActionListener value="supprimer" target="#{form.action}"/>
</p:commandButton>
...
</p:confirmDialog>
- 第 2 行:将执行 [Form].action 方法,
- 第 4 行:在执行之前,action 字段已被设置为 'delete'。
[action] 方法如下:
// action on RV
public void action() {
// according to desired action
if (action.equals("supprimer")) {
supprimer();
}
...
}
public void supprimer() {
// is there anything we can do?
Rv rv = creneauChoisi.getRv();
if (rv == null) {
signalerActionIncorrecte();
return;
}
try {
// deleting an appointment
application.getMetier().supprimerRv(rv);
// updating the agenda
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// form2 is displayed
setForms(true, true, false);
} catch (Throwable th) {
// error view
prepareVueErreur(th);
}
// raz of the chosen slot
creneauChoisi = null;
}
- 第 4 行:如果操作是 'delete',则执行 [delete] 方法,
- 第 12 行:检索所选时段的预约。请注意,[selectedSlot] 已初始化为所选 [DoctorSlot] 元素的引用;
- 如果该预约存在,则将其删除(第19行),刷新日历(第21行)并重新显示(第23行),
- 如果删除失败,则显示错误页面(第 26 行),
- 如果所选时段没有预约(第 13 行),则说明用户在没有预约的时段上点击了 [删除]。我们将报告此错误:
![]() |
[reportIncorrectAction] 方法如下:
// report an incorrect action
private void signalerActionIncorrecte() {
// raz selected slot
creneauChoisi = null;
// error
msgErreur = Messages.getMessage(null, "form2.erreurAction", null).getSummary();
RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
}
- 第 4 行:移除选中状态,
- 第 6 行:生成国际化的错误消息,
- 第 7 行:将属性 ('error', true) 添加到 AJAX 调用的 args 字典中。
让我们回到 [Yes] 按钮的 XHTML 代码:
<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
oncomplete="handleRequest(xhr, status, args); confirmation.hide()">
- 第 2 行:在 [Form].action 方法执行后,将执行 JavaScript 的 handleRequest 方法:
<!-- message d'erreur -->
<p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >
<h:outputText value="#{form.msgErreur}" />
</p:dialog>
<!-- gestion du retour serveur -->
<script type="text/javascript">
function handleRequest(xhr, status, args) {
// erreur ?
if(args.erreur) {
dlgErreur.show();
}
}
</script>
- 第 10 行:我们检查 args 字典是否包含名为 'error' 的属性。如果包含,则显示第 2 行中的对话框。
- 第 3 行:显示由模板生成的错误消息。
6.11. 预约
预约操作对应以下流程:
![]() |
本案涉及的观点如下:
<!-- context menu -->
<p:contextMenu for="agenda">
<p:menuitem value="#{msg['form2.reserver']}" update=":formulaire:contenu" action="#{form.action}" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="reserver" target="#{form.action}"/>
</p:menuitem>
...
</p:contextMenu>
<!-- agenda -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- schedule column -->
<p:column style="width: 100px">
...
</p:column>
<!-- customer column -->
<p:column style="width: 300px">
<f:facet name="header">
<h:outputText value="#{msg['form2.client']}"/>
</f:facet>
...
<ui:fragment rendered="#{creneauMedecinJour.rv==null and form.creneauChoisi!=null and form.creneauChoisi.creneau.id==creneauMedecinJour.creneau.id}">
<p:autoComplete completeMethod="#{form.completeClients}" value="#{form.identiteClient}" size="30"/>
<p:spacer width="50px"/>
<p:commandLink action="#{form.action()}" value="#{msg['form2.valider']}" update=":formulaire:contenu" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="valider" target="#{form.action}"/>
</p:commandLink>
<p:spacer width="50px"/>
<p:commandLink action="#{form.action()}" value="#{msg['form2.annuler']}" update=":formulaire:contenu">
<f:setPropertyActionListener value="annuler" target="#{form.action}"/>
</p:commandLink>
</ui:fragment>
</p:column>
</p:dataTable>
...
- 第 21–31 行:显示以下内容:
![]() |
- 第 21 行:当没有预约、已进行选择且所选时段的 ID 与当前表格行中的 ID 匹配时,将显示此内容。如果未包含此条件,则该片段将显示在所有时段中,
- 第 22 行:该输入字段将是一个辅助输入字段。我们在此假设可能存在多个客户端,
- 第 24–26 行:[提交] 链接,
- 第28–30行:[取消]链接。
自动完成字段由以下代码生成:
<p:autoComplete completeMethod="#{form.completeClients}" value="#{form.identiteClient}" size="30"/>
[Form].completeClients 方法负责根据用户在输入字段中输入的字符向用户提供建议:
![]() |
建议的格式为 [姓, 名, 头衔]。[Form].completeClients 方法的代码如下:
// the autocomplete text method
public List<String> completeClients(String query) {
List<String> identites = new ArrayList<String>();
// we look for customers who match
for (Client c : clients) {
String identite = identite(c);
if (identite.toLowerCase().startsWith(query.toLowerCase())) {
identites.add(identite);
}
}
return identites;
}
private String identite(Client c) {
return c.getNom() + " " + c.getPrenom() + " " + c.getTitre();
}
- 第 2 行:query 是用户输入的字符串,
- 第 3 行:建议列表。初始为空列表,
- 第5–10行:我们构建客户的全名 [姓, 名, 头衔]。如果全名以查询字符串开头(第7行),则将其添加到建议列表中(第8行)。
6.12. 确认预约
确认预约的流程如下:
![]() |
[验证]链接的代码如下:
<p:commandLink action="#{form.action()}" value="#{msg['form2.valider']}" update=":formulaire:contenu" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="valider" target="#{form.action}"/>
</p:commandLink>
因此,[Form].action() 方法将处理此事件。与此同时,[Form].action 模型将接收字符串 'submit'。代码如下:
// bean Application
@Inject
private Application application;
// session cache
...
private Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
// model
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
private AgendaMedecinJour agendaMedecinJour;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
private Boolean erreur = false;
private String identiteClient;
private String action;
private String msgErreur;
@PostConstruct
private void init() {
...
for (Client c : clients) {
hClients.put(c.getId(), c);
hIdentitesClients.put(identite(c), c);
}
}
// action on RV
public void action() {
// according to desired action
...
if (action.equals("valider")) {
validerResa();
}
}
// rv validation
public void validerResa() {
// reservation validation
try {
// does the customer exist?
Boolean erreur = !hIdentitesClients.containsKey(identiteClient);
if (erreur) {
msgErreur = Messages.getMessage(null, "form2.erreurClient", new Object[]{identiteClient}).getSummary();
RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
return;
}
// we add the Rv
application.getMetier().ajouterRv(jour, creneauChoisi.getCreneau(), hIdentitesClients.get(identiteClient));
// updating the agenda
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// form2 is displayed
setForms(true, true, false);
} catch (Throwable th) {
// error view
prepareVueErreur(th);
}
// raz of the chosen slot
creneauChoisi = null;
// raz client
identiteClient = null;
}
- 第 33-35 行:由于 action 字段的值,将执行 [validateReservation] 方法,
- 第 43 行:我们首先检查客户是否存在。这是因为在辅助输入字段中,用户可能未使用提供的建议而手动输入了数据。辅助输入与 [Form].identiteClient 模型相关联。 因此,我们检查该身份是否存在于模型实例化时创建的 identitesClients 字典中(第 20 行)。该字典将类型为 [Last name, First name, Title] 的客户身份与客户本身相关联(第 25 行),
- 第 44 行:如果客户不存在,则向浏览器返回错误,
- 第 45 行:显示一条国际化错误消息,
- 第 46 行:我们将属性 ('error', true) 添加到 AJAX 调用的 args 字典中。该 AJAX 调用已定义如下:
<p:commandLink action="#{form.action()}" value="#{msg['form2.valider']}" update=":formulaire:contenu" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="valider" target="#{form.action}"/>
</p:commandLink>
在上文第 3 行中,我们可以看到 [Validate] 链接有一个 oncomplete 属性。正是这个属性将使用我们之前遇到过的一种技术来显示错误消息。
- 第 50 行:我们请求 [business] 层为选定的日期 (day)、选定的时段 (creneauChoisi.getCreneau()) 以及选定的客户 (hIdentitesClients.get(identiteClient)) 添加一个预约,
- 第 52 行:我们请求 [业务] 层刷新医生的日程表。我们将看到新增的预约,以及应用程序的其他用户可能做出的任何更改,
- 第 54 行:重新显示日历 [form2.xhtml],
- 第 57 行:若发生错误,则显示错误页面。
6.13. 取消预约
这对应于以下流程:
![]() |
[form2.xhtml] 页面上的 [取消] 按钮如下所示:
<p:commandLink action="#{form.action()}" value="#{msg['form2.annuler']}" update=":formulaire:contenu">
<f:setPropertyActionListener value="annuler" target="#{form.action}"/>
</p:commandLink>
因此,将调用 [Form].action 方法:
// action sur RV
public void action() {
// selon l'action désirée
...
if (action.equals("annuler")) {
annulerRv();
}
}
// annulation prise de Rdv
public void annulerRv() {
// on affiche form2
setForms(true, true, false);
// raz du créneau choisi
creneauChoisi = null;
// raz client
identiteClient = null;
}
6.14. 浏览日历
工具栏可用于浏览日历:
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
虽然上方的截图中未显示,但日历已更新为新选定日期的日程安排。
在 [form1.xhtml] 中,这三个相关按钮的标签如下:
<p:toolbar>
<p:toolbarGroup align="left">
...
<h:outputText value="#{msg['form1.jour']}"/>
<p:calendar id="calendrier" value="#{form.jour}" readOnlyInputText="true">
<p:ajax event="dateSelect" listener="#{form.hideAgenda}" update=":formulaire:contenu"/>
</p:calendar>
<p:separator/>
<p:commandButton id="resa-agenda" icon="ui-icon-check" actionListener="#{form.getAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-agenda" value="#{msg['form1.agenda']}"/>
<p:commandButton id="resa-precedent" icon="ui-icon-seek-prev" actionListener="#{form.getPreviousAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-precedent" value="#{msg['form1.precedent']}"/>
<p:commandButton id="resa-suivant" icon="ui-icon-seek-next" actionListener="#{form.getNextAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-suivant" value="#{msg['form1.suivant']}"/>
<p:commandButton id="resa-today" icon="ui-icon-home" actionListener="#{form.today}" update=":formulaire:contenu"/>
<p:tooltip for="resa-today" value="#{msg['form1.today']}"/>
</p:toolbarGroup>
<p:toolbarGroup align="right">
...
</p:toolbarGroup>
</p:toolbar>
方法 [Form].getPreviousAgenda、[Form].getNextAgenda 和 [Form].today 如下所示:
private Date jour = new Date();
public void getPreviousAgenda() {
// on passe au jour précédent
Calendar cal = Calendar.getInstance();
cal.setTime(jour);
cal.add(Calendar.DAY_OF_YEAR, -1);
jour = cal.getTime();
// agenda
if (form2Rendered) {
getAgenda();
}
}
public void getNextAgenda() {
// on passe au jour suivant
Calendar cal = Calendar.getInstance();
cal.setTime(jour);
cal.add(Calendar.DAY_OF_YEAR, 1);
jour = cal.getTime();
// agenda
if (form2Rendered) {
getAgenda();
}
}
// agenda aujourd'hui
public void today() {
jour = new Date();
// agenda
if (form2Rendered) {
getAgenda();
}
}
- 第 1 行:日历显示的日期,
- 第 5 行:我们使用一个日历,
- 第6行:该日历初始化为当前日期,
- 第 7 行:从日历中减去一天,
- 第 8 行:并将它重置为日历显示日期,
- 第 11 行:如果日历当前正在显示,则重新绘制日历。这是因为即使用户未显示日历,也可以使用工具栏。
其他方法类似。
6.15. 更改显示语言
语言切换通过工具栏上的菜单按钮进行管理:
![]() |
![]() |
菜单按钮的标签如下:
<p:toolbar>
<p:toolbarGroup align="left">
...
</p:toolbarGroup>
<p:toolbarGroup align="right">
<p:menuButton value="#{msg['form1.options']}">
<p:menuitem id="menuitem-francais" value="#{msg['form1.francais']}" actionListener="#{form.setFrenchLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-anglais" value="#{msg['form1.anglais']}" actionListener="#{form.setEnglishLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>
</p:menuButton>
</p:toolbarGroup>
</p:toolbar>
在模板中执行的方法如下:
private String locale = "fr";
public void setFrenchLocale() {
locale = "fr";
// reload page
redirect();
}
public void setEnglishLocale() {
locale = "en";
// reload page
redirect();
}
private void redirect() {
// redirect the client to the servlet
ExternalContext ctx = FacesContext.getCurrentInstance().getExternalContext();
try {
ctx.redirect(ctx.getRequestContextPath());
} catch (IOException ex) {
Logger.getLogger(Form.class.getName()).log(Level.SEVERE, null, ex);
}
}
第 3 行和第 9 行中的方法只是初始化第 1 行中的 local 字段,然后将客户端浏览器重定向到同一页面。重定向是一种响应,其中服务器指示浏览器加载另一个页面。随后,浏览器会向该新页面发送一个 GET 请求。
- 第 17 行:[ExternalContext] 是一个 JSF 类,用于访问当前正在运行的 Servlet,
- 第19行:我们执行重定向操作。redirect方法的参数是客户端浏览器应被重定向到的页面的URL。这里我们希望重定向到 [/mv-rdvmedecins-pf],即我们应用程序的名称:
![]() |
[getRequestContextPath] 方法提供了此名称。因此,将加载我们应用程序的首页 [index.xhtml]。该页面与会话范围内的 [Form] 模型相关联。该模型管理着三个布尔值,用于控制 [index.xhtml] 页面的显示效果:
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
由于模型属于会话作用域,这三个布尔变量保留了其值。因此,[index.xhtml] 页面将显示为重定向前的样子。该页面使用以下 Facelet 模板 [layout.xhtml] 进行格式化:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{form.locale}">
....
</f:view>
</html>
第 9 行中的标签通过其 locale 属性设置了页面的显示语言。因此,页面将根据情况切换为法语或英语。那么,为什么要进行重定向呢?让我们回到语言切换选项的标签:
<p:toolbar>
<p:toolbarGroup align="left">
...
</p:toolbarGroup>
<p:toolbarGroup align="right">
<p:menuButton value="#{msg['form1.options']}">
<p:menuitem id="menuitem-francais" value="#{msg['form1.francais']}" actionListener="#{form.setFrenchLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-anglais" value="#{msg['form1.anglais']}" actionListener="#{form.setEnglishLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>
</p:menuButton>
</p:toolbarGroup>
</p:toolbar>
这些代码最初是通过 AJAX 调用(第 7 行和第 8 行中的 update 属性)来更新表单 ID 区域的。但在测试过程中,语言切换功能并不总是有效。因此,我们采用了重定向来解决此问题。我们也可以在标签上设置 ajax='false' 属性来触发页面刷新,这样就能避免重定向。
6.16. 刷新列表
这对应于以下操作:
![]() |
与 [刷新] 选项关联的标签如下:
<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>
[Form].refresh 方法如下:
public void refresh() {
// on rafraîchit les listes
init();
}
init 方法是在 [Form] Bean 构建完成后立即执行的方法。其目的是将数据库中的数据缓存到模型中:
// bean Application
@Inject
private Application application;
// session cache
private List<Medecin> medecins;
private List<Client> clients;
private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
private Map<Long, Client> hClients = new HashMap<Long, Client>();
private Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
...
@PostConstruct
private void init() {
// caching doctors and customers
try {
medecins = application.getMetier().getAllMedecins();
clients = application.getMetier().getAllClients();
} catch (Throwable th) {
...
}
...
// dictionaries
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
hIdentitesClients.put(identite(c), c);
}
}
init 方法在第 5 至 9 行构建了列表和字典。这种方法的缺点是,这些元素不再反映数据库中的变化(例如新增客户、医生等)。refresh 方法会强制重建这些列表和字典。因此,每当数据库发生更改(例如添加新客户)时,我们都会调用该方法。
6.17. 结论
让我们回顾一下刚刚构建的应用程序的架构:
![]() |
我们主要依赖于预构建的 JSF2 版本:
- 保留了 [业务]、[DAO] 和 [JPA] 层,
- Web 层中的 [Application] 和 [Form] Bean 被保留,但因用户界面的增强,向其中添加了新功能,
- 用户界面已进行了重大修改。特别是,它现在功能更加丰富,且更易于用户使用。
从 JSF 转向 PrimeFaces 来构建 Web 界面需要一定的经验,因为起初面对海量的可用组件会让人有些不知所措,最终也不太确定该使用哪些组件。因此,必须专注于界面所需的可用性。
6.18. Eclipse 测试
与之前版本的示例应用程序一样,我们将演示如何使用 Eclipse 测试此 03 版。首先,我们将示例 03 [1] 的 Maven 项目导入 Eclipse:
![]() |
- [mv-rdvmedecins-ejb-dao-jpa]:[DAO] 和 [JPA] 层,
- [mv-rdvmedecins-ejb-metier]:[业务]层,
- [mv-rdvmedecins-pf]:使用 JSF 和 PrimeFaces 实现的 [Web] 层,
- [mv-rdvmedecins-app]:企业项目 [mv-rdvmedecins-app-ear] 的父项目。导入父项目时,子项目会自动被导入,
- 在 [2] 中,运行企业项目 [mv-rdvmedecins-app-ear],
![]() |
- 在 [3] 中,选择 Glassfish 服务器,
- 在 [4] 中,进入 [Servers] 选项卡,应用程序已部署。它不会自动运行。您必须在浏览器中输入其 URL [http://localhost:8080/mv-rdvmedecins-pf/] [5]:
![]() |
















































