Skip to content

9. 示例应用程序 05:rdvmedecins-pfm-ejb

让我们回顾一下为 GlassFish 服务器开发的 JSF/EJB 示例应用程序 01 的结构:

除了Web层之外,我们不会对该架构进行任何更改。Web层将在此处使用JSF、PrimeFaces和PrimeFaces Mobile来实现。目标浏览器将是移动浏览器。

我们已使用GlassFish开发了两个应用程序:

  • 应用程序01采用JSF/EJB架构,界面较为基础,
  • 应用程序 03 采用了 PF/EJB 架构,拥有功能丰富的界面。

鉴于 PrimeFaces Mobile 提供的组件数量有限,我们将恢复使用应用程序 01 的基本界面。此外,视图必须能够适应移动设备的小屏幕尺寸。

9.1. 视图

为了让您直观了解,以下是该应用程序在 iPhone 4 模拟器上运行的几张截图:

  • [1] 中的主页。请注意,这次我们必须指定机器名称(也可以输入其 IP 地址),因为使用 localhost 时屏幕上没有任何显示,
  • [2] 中的医生下拉菜单,
  • 在 [3] 中,选择预约的日期,
  • 在 [4] 中,是请求查看当日日程的按钮,
  • 在 [5] 中,新视图显示了医生的可用时段,
  • 在 [6] 中,是一组用于浏览日历的按钮,
  • 在 [7] 处,显示提醒您医生姓名及日期的提示信息,
  • 在 [8] 中,可预订的时间段。开始吧,
  • 在 [9] 中,即用户的选项视图,
  • 在[10]中,一条提醒用户医生、日期及预约时段的消息,
  • 在[11]中,客户端下拉菜单,
  • 在 [12] 中,确认按钮,
  • 在 [13] 中,点击该按钮将返回日历,
  • 在 [14] 中,已预订的时间段现已保留。现在我们将删除该预订,
  • 在[15]中,我们仍处于同一视图,
  • 但在[16]中,该预约已被删除,

在首页上,您可以更改语言 [17]、[18]、[19]:

最后,我们还添加了一个错误页面:

9.2. NetBeans 项目

NetBeans 项目如下:

  
  • [mv-rdvmedecins-ejb-dao-jpa]:示例 01 中 [DAO] 和 [JPA] 层的 EJB 项目,
  • [mv-rdvmedecins-ejb-metier]:示例 01 [业务] 层的 EJB 项目,
  • [mv-rdvmedecins-pfmobile]: [Web] 层项目 / PrimeFaces Mobile – 新增,
  • [mv-rdvmedecins-pfmobile-app-ear]:用于在 GlassFish 服务器上部署应用程序的企业级项目 – 新增。

9.3. 该企业项目

该企业项目仅用于将三个模块 [mv-rdvmedecins-ejb-dao-jpa]、[mv-rdvmedecins-ejb-metier]、[mv-rdvmedecins-pfmobile] 部署到 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>
...
 
  <groupId>istia.st</groupId>
  <artifactId>mv-rdvmedecins-pfmobile-app-ear</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>ear</packaging>
 
  <name>mv-rdvmedecins-pfmobile-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-pfmobile</artifactId>
            <version>${project.version}</version>
            <type>war</type>
        </dependency>
    </dependencies>
</project>
  • 第 6–9 行:企业项目的 Maven 工件,
  • 第 14–33 行:该项目的三个依赖项。请注意它们的类型(第 19、25、31 行)。

要运行 Web 应用程序,必须运行此企业项目。

9.4. PrimeFaces Mobile Web 项目

PrimeFaces Mobile Web 项目如下:

  • 在 [1] 中,是该项目的页面。[index.xhtml] 页面是该项目唯一的页面。它包含五个视图:[vue1.xhtml]、[vue2.xhtml]、[vue3.xhtml]、[vueErreurs.xhtml] 和 [config.xhtml],
  • 在 [2] 中,是 Java Bean。其中 [Application] Bean 具有应用程序作用域,而 [Form] Bean 具有会话作用域。[Error] 类封装了一个错误,
  • 在 [3] 中,是用于国际化的消息文件,
  • 在 [4] 中,是依赖项。Web 项目依赖于 EJB 项目的 [DAO] 层、EJB 项目的 [business] 层以及 PrimeFaces Mobile 的 [web] 层。

9.5. 项目配置

该项目的配置与我们之前学习过的 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.PROJECT_STAGE</param-name>
    <param-value>Development</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>

请注意,第 26 行中的 [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>
    <default-render-kit-id>PRIMEFACES_MOBILE</default-render-kit-id>
  </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>

[messages_fr.properties]:法语消息文件


# page
page.titre=Les M\u00e9decins Associ\u00e9s
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy
 
# vue1
vue1.header=Les M\u00e9decins Associ\u00e9s - R\u00e9servations
 
# formulaire 1
form1.titre=R\u00e9servations
form1.medecin=M\u00e9decin
form1.jour=Jour (jj/mm/aaaa)
form1.date.requise=La date est n\u00e9cessaire
form1.date.invalide=La date est invalide
form1.date.invalide_detail=La date est invalide
form1.agenda=Agenda
form1.options=Options
 
# formulaire 2
form2.titre={0} {1} {2}<br/>{3}
form2.titre_detail={0} {1} {2}<br/>{3}
form2.retour=Retour
form2.supprimer=Supprimer
form2.reserver=R\u00e9server
form2.precedent=Jour pr\u00e9c\u00e9dent
form2.suivant=Jour suivant
form2.today=Aujourd'today
 
# formulaire 3
form3.client=Client
form3.valider=Valider
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.retour=Retour
 
# erreur
erreur.titre=Une erreur s'is produced.
 
# config
config.retour=Retour
config.titre=Configuration
config.langue=Langue
config.langue.francais=Fran\u00e7ais
config.langue.anglais=Anglais
config.valider=Valider
 
#exception
exception.titre=Application indisponible. Veuillez recommencer ult\u00e9rieurement.

[messages_en.properties]:英文消息文件


# page
page.titre=The Associated Doctors
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy
 
# vue1
vue1.header=The Associated Doctors - Reservations
 
# formulaire 1
form1.titre=Reservations
form1.medecin=Doctor
form1.jour=day (dd/mm/yyyy)
form1.date.requise=The date is necessary
form1.date.invalide=Invalid date
form1.date.invalide_detail=invalid date
form1.agenda=Diary
form1.options=Options

# formulaire 2
form2.titre={0} {1} {2}<br/>{3}
form2.titre_detail={0} {1} {2}<br/>{3}
form2.retour=Back
form2.supprimer=Delete
form2.reserver=Reserve
form2.precedent=Previous Day
form2.suivant=Next Day
form2.today=Today
 
# formulaire 3
form3.client=Patient
form3.valider=Validate
form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.retour=Back
 
# erreur
erreur.titre=Some error happened
 
# config
config.retour=Back
config.titre=Configuration
config.langue=Language
config.langue.francais=French
config.langue.anglais=English
config.valider=Validate
 
#exception
exception.titre=Application not available. Please try again later.

9.6. [index.xhtml] 页面

该项目始终显示同一页面,即以下 [index.xhtml] 页面:


<f:view xmlns="http://www.w3.org/1999/xhtml"
        xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html"
        xmlns:ui="http://java.sun.com/jsf/facelets"
        xmlns:p="http://primefaces.org/ui"
        xmlns:pm="http://primefaces.org/mobile"
        contentType="text/html"
        locale="#{form.locale}">
 
  <pm:page title="#{msg['page.titre']}">
    <pm:view id="vue1">
      <ui:fragment rendered="#{form.form1Rendered}">
        <ui:include src="vue1.xhtml"/>
      </ui:fragment>
      <ui:fragment rendered="#{form.erreurInit}">
        <ui:include src="vueErreurs.xhtml"/>
      </ui:fragment>
    </pm:view>
    <pm:view id="vue2">
      <ui:fragment rendered="#{form.form2Rendered}">
        <ui:include src="vue2.xhtml"/>
      </ui:fragment>
    </pm:view>
    <pm:view id="vue3">
      <ui:fragment rendered="#{form.form3Rendered}">
        <ui:include src="vue3.xhtml"/>
      </ui:fragment>
    </pm:view>
    <pm:view id="vueErreurs">
      <ui:fragment rendered="#{form.erreurRendered}">
        <ui:include src="vueErreurs.xhtml"/>
      </ui:fragment>
    </pm:view>
    <pm:view id="config">
      <ui:include src="config.xhtml"/>
    </pm:view>
  </pm:page>    
</f:view>
  • 第 8 行:该页面已实现国际化(locale 属性),
  • 第 10 行:该页面包含五个视图:第 11 行的 view1、第 19 行的 view2、第 24 行的 view3、第 29 行的 viewErrors 以及第 34 行的 config。在任何给定时刻,这些视图中仅有一个可见。应用程序启动时,将显示 view1。 在此,我们遇到了以下问题:如果应用程序初始化成功,则必须显示 [view1.xhtml];否则,必须显示 [errorView.xhtml]。 我们通过确保 vue1 视图的内容由模型管理来解决此问题,该模型通过设置布尔值 [Form].form1rendered(第 12 行)和 [Form].erreurInit(第 15 行)来确定 vue1 的内容(第 11 行),

在模拟器中,视图 [vue1.xhtml] 渲染为 [1],视图 [vue2.xhtml] 渲染为 [2],视图 [vue3.xhtml] 渲染为 [3]:

视图 [vueErreurs.xhtml] 的渲染结果为 [4],视图 [config.xhtml] 的渲染结果为 [5]:

9.7. 项目的 Bean

[utils] 包中的类已作介绍:[Messages] 类是一个用于简化应用程序消息国际化的类。相关内容已在第 2.8.5.7 节中讨论过。

9.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容器将向其中注入对[业务]层本地接口的引用。让我们回顾一下应用程序架构:

PFM 应用程序和 [Business] EJB 将运行在同一个 JVM(Java 虚拟机)中。因此,[PFM] 层将使用 EJB 的本地接口。仅此而已。[Application] Bean 不再包含其他内容。其他 Bean 若要访问 [Business] 层,将从该 Bean 中获取它。

9.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 行:错误消息。

9.7.3. [Form] Bean

其代码如下:


package beans;
 
...
 
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
 
  public Form() {
  }
  // bean Application
  @Inject
  private Application application;
  private IMetierLocal metier;
  // 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>();
  // model
  private Long idMedecin;
  private Date jour = new Date();
  private String strJour;
  private Boolean form1Rendered;
  private Boolean form2Rendered;
  private Boolean form3Rendered;
  private Boolean erreurRendered;
  private String form2Titre;
  private String form3Titre;
  private AgendaMedecinJour agendaMedecinJour;
  private Long idCreneauChoisi;
  private Medecin medecin;
  private Long idClient;
  private CreneauMedecinJour creneauChoisi;
  private List<Erreur> erreurs;
  private Boolean erreurInit = false;
  private String action;
  private String locale = "fr";
  private String msgErreurDate = "";
  private SimpleDateFormat dateFormatter;
  private Boolean erreurDate;
  
  @PostConstruct
  private void init() {
    System.out.println("init");
    // initially no error
    erreurInit = false;
    // date formatting
    dateFormatter = new SimpleDateFormat(Messages.getMessage(null, "format.date", null).getSummary());
    dateFormatter.setLenient(false);
    // the current day
    strJour = dateFormatter.format(jour);
    // recover the business layer
    metier = application.getMetier();
    // caching doctors and customers
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // we note the error
      erreurInit = true;
      prepareVueErreur(th);
      return;
    }
    // list checking
    if (medecins.size() == 0) {
      // we note the error
      erreurInit = true;
      erreurs = new ArrayList<Erreur>();
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // we note the error
      erreurInit = true;
      erreurs = new ArrayList<Erreur>();
      erreurs.add(new Erreur("", "La liste des clients est vide"));
    }
    // mistake?
    if (erreurInit) {
      // the error view is displayed
      setForms(false, false, false, true);
      return;
    }
 
    // dictionaries
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
    }
    // view 1
    setForms(true, false, false, false);
  }
 
  // view display
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.form3Rendered = form3Rendered;
    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(false, false, false, true);
  }
 
  // getters and setters
  ..
}
  • 第 5-7 行:[Form] 类是一个名为“form”且作用域为会话的 Bean。请注意,因此该类必须是可序列化的,
  • 第 12–13 行:表单 Bean 持有对应用程序 Bean 的引用。该引用将由应用程序运行的 Servlet 容器注入(@Inject 注解的存在)。
  • 第 24–27 行:控制视图 vue1(第 24 行)、vue2(第 25 行)、vue3(第 26 行)和 vueErrors(第 27 行)的显示,
  • 第 43–44 行:类实例化后会立即执行 init 方法(存在 @PostConstruct 注解),
  • 第 49–50 行:处理日期格式。PrimeFaces Mobile 不提供日历控件。此外,JSF 验证器无法在 PFM 页面中使用。因此,我们需要手动处理日历日期的输入,
  • 第 49 行:设置日期格式。该格式取自国际化文件:

format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy
  • 第 50 行:我们指定在日期处理方面绝不能“宽容”。如果不这样做,像 12/32/2011 这样的输入(这是错误的输入)会被视为有效日期 01/01/2012,
  • 第 54 行:我们从 [Application] Bean 中获取 [business] 层的引用,
  • 第 57-58 行:我们向 [business] 层请求医生和客户的列表,
  • 第 66–91 行:若一切顺利,将构建医生和客户的字典。它们按编号进行索引。随后,将显示 [vue1.xhtml] 视图(第 93 行),
  • 第 59 行:若发生错误,则构建页面模板 [errorView.xhtml]。该模板即第 35 行中的错误列表,
  • 第 105–115 行:[prepareVueErreur] 方法构建待显示的错误列表。随后 [index.xhtml] 页面显示 [vueErreurs.xhtml] 视图(第 114 行)。

[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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">
 
  <!-- Errors view -->
  <pm:header title="#{msg['page.titre']}" swatch="b">
    <f:facet name="left">
      <p:button icon="home" value=" " href="#vue1?reverse=true" />
    </f:facet>
  </pm:header>
  <pm:content>
    <div align="center">
      <h1><h:outputText value="#{msg['erreur.titre']}" style="color: blue"/></h1>
    </div>
 
    <p:dataList value="#{form.erreurs}" var="erreur">
      <b>#{erreur.classe}</b> : <i>#{erreur.message}</i>
    </p:dataList>
  </pm:content>
</html>

它使用 <p:dataList> 标签(第 21–23 行)来显示错误列表。第 13 行的按钮可让您返回 vue1 视图。

  • [1] 按钮由第 13 行生成。icon 属性用于设置按钮的图标。该按钮将返回 vue1 视图(href 属性),
  • 标题 [2] 由第 11–15 行生成,
  • 标题 [3] 由第 18 行生成,
  • 文本 [4] 由第 22 行中的模板 #{error.class} 生成,
  • 文本 [5] 由第 22 行中的模板 #{erreur.message} 生成。

接下来我们将定义应用程序生命周期的各个阶段。针对每个用户操作,我们将分析相关的视图以及其中触发的事件处理程序。

9.8. 显示主页

如果一切正常,首先显示的视图是 [vue1.xhtml]。这将生成以下视图:

[vue1.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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">
 
  <!-- View 1 -->
  <pm:header title="#{msg['page.titre']}" swatch="b">
    <f:facet name="left">
      <p:button icon="gear" value=" "  href="#config" />
    </f:facet>
  </pm:header>
  <pm:content>
    <h:form id="form1">
      <div align="center">
        <h1><h:outputText value="#{msg['form1.titre']}" style="color: blue"/></h1>
      </div>
      <pm:field>
        <h:outputLabel value="#{msg['form1.medecin']}" for="choixMedecin"/>
        <h:selectOneMenu id="choixMedecin" value="#{form.idMedecin}">  
          <f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>  
        </h:selectOneMenu>              
      </pm:field>
      <pm:field>
        <h:outputLabel value="#{msg['form1.jour']}" for="jour"/>
        <p:inputText id="jour" value="#{form.strJour}"/>
        <ui:fragment rendered="#{form.erreurDate}">
          <p:spacer width="50px"/>
          <h:outputText id="msgErreurDate" value="#{form.msgErreurDate}" style="color: red"/>
        </ui:fragment>
      </pm:field>
      <p:commandButton value="#{msg['form1.agenda']}" update=":form1, :vue2, :vueErreurs" action="#{form.getAgenda}" />
    </h:form>
  </pm:content>
</html>
  • 第 11–15 行:生成标题 [1],
  • 第 13 行:创建 [2] 按钮。点击它将显示配置视图(href 属性),
  • 第 19 行:显示标题 [3],
  • 第 21–26 行:生成医生下拉菜单 [4],
  • 第 27–34 行:显示日期输入框 [5]。该输入框使用不带验证器的 <p:inputText> 标签。日期验证将在服务器端进行。如果日期不正确,服务器将设置一条错误消息,由第 30–34 行显示,
  • 第 35 行:提交表单的按钮。它会更新三个区域:form1(vue1 中的表单)、vue2 vueErrors。实际上,如果日期无效,必须更新 form1;如果日期正确,则必须更新 vue2。 最后,如果发生异常(例如数据库连接中断),则必须显示 vueErrors。您可能会想使用 vue1 代替 form1(更新整个视图)。但在这种情况下,应用程序将会崩溃。

该视图由以下模型提供支持:


@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
 
  public Form() {
  }
 
  // bean Application
  @Inject
  private Application application;
  private IMetierLocal metier;
  // session cache
  private List<Medecin> medecins;
  private List<Client> clients;
  // model
  private Long idMedecin;
  private Date jour = new Date();
  private String strJour;
  private Boolean form1Rendered;
  private Boolean form2Rendered;
  private Boolean form3Rendered;
  private Boolean erreurRendered;
  private String msgErreurDate = "";
  private Boolean erreurDate;
  
    // list of doctors
  public List<Medecin> getMedecins() {
    return medecins;
  }
 
  // agenda
  public String getAgenda() {
...
    }
  }
  • 第 16 行的字段用于读取和写入页面第 23 行列表的值。当页面首次显示时,它会设置下拉列表中选中的值,
  • 第27–29行的方法生成医生下拉列表(视图的第24行)中的选项。每个生成的选项将使用医生的头衔姓氏名字作为标签(itemLabel),并使用医生的ID作为值(itemValue)。
  • 第 18 行的字段为页面第 29 行的输入字段提供读写访问权限,
  • 第 32–34 行:getAgenda 方法处理页面第 35 行 [Agenda] 按钮的点击事件。其代码如下:

// agenda
  public String getAgenda() {
    try {
      // on vérifie le jour
      jour = dateFormatter.parse(strJour);
      // pas d'erreur
      erreurDate=false;
      msgErreurDate = "";
      // on crée l'agenda
      return getAgenda(jour);
    } catch (ParseException ex) {
      // msg d'erreur
      erreurDate=true;
      msgErreurDate = Messages.getMessage(null, "form1.date.invalide", null).getSummary();
      // vue1      
      setForms(true, false, false, false);
      return "pm:vue1";
    }
  }
  • 该方法首先检查用户输入的日期是否有效,
  • 第 5 行:根据模型实例化时由 init 方法初始化的日期格式解析日期,
  • 第 11 行:如果发生异常,则设置错误(第 13 行),构建国际化错误消息(第 14 行),准备 vue1 视图(第 16 行),并显示 vue1 视图(第 17 行),
  • 第 10 行:如果日期有效,则执行以下方法:

  // agenda
  public String getAgenda(Date jour) {
    // no slots selected yet
    creneauChoisi = null;
    try {
      // we pick up the chosen doctor
      medecin = hMedecins.get(idMedecin);
      // title form 2
      form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
      // the doctor's diary for a given day
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // view 2 is displayed
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      System.out.println(th);
      // error view
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
}

这里我们看到一段之前出现过多次的代码。在第 9 行,为 vue2 视图构建了一条国际化消息:


form2.titre={0} {1} {2}<br/>{3}
form2.titre_detail={0} {1} {2}<br/>{3}

请注意,我们在消息中加入了 XHTML 内容。它将显示如下:

9.9. 显示医生的日程安排

医生的日程安排由视图 [vue2.xhtml] 显示:

[vue2.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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">
 
  <!-- View 2 -->
  <pm:header title="#{msg['page.titre']}" swatch="b"/>
  <pm:content>
    <h:form id="form2">
      <div align="center">
        <pm:buttonGroup orientation="horizontal">
          <p:commandButton inline="true" icon="back" value=" " action="#{form.showVue1}" update=":vue1"/>
          <p:commandButton inline="true" icon="minus" value=" " action="#{form.getPreviousAgenda}" update=":form2"/>
          <p:commandButton inline="true" icon="home" value=" " action="#{form.getTodayAgenda}" update=":form2"/>
          <p:commandButton inline="true" icon="plus" value=" " action="#{form.getNextAgenda}" update=":form2"/>
        </pm:buttonGroup>
        <h3><h:outputText value="#{form.form2Titre}" style="color: blue" escape="false"/></h3>
      </div>
 
      <p:dataList id="creneaux" type="inset" value="#{form.agendaMedecinJour.creneauxMedecinJour}" var="creneauMedecinJour">
        <p:column>
          <div align="center">
            <h2>
              <h:outputFormat value="{0,number,#00}h:{1,number,#00} - {2,number,#00}h:{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>
              <ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
                <br/>
                <h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}" style="color: blue"/>
              </ui:fragment>
            </h2>
          </div>
          <div align="center">
            <ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
              <p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.supprimer']}" icon="minus" update=":form2, :vue3, :vueErreurs">
                <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
              </p:commandButton>
            </ui:fragment>
            <ui:fragment rendered="#{creneauMedecinJour.rv==null}">
              <p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.reserver']}" icon="plus" update=":form2, :vue3, :vueErreurs">
                <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
              </p:commandButton>
            </ui:fragment>
          </div>
        </p:column>
      </p:dataList>
    </h:form>
  </pm:content>
</html>
  • 第 11 行:生成标题 [1],
  • 第 15–20 行:生成按钮组 [2],
  • 第 21 行:生成标题 [3]。请注意 escape 属性的值。正是这个属性使得我们放置在 form2Titre 中的 XHTML 代码能够被正确解析,
  • 第 24 行:使用 dataList 显示时间段,
  • 第 28–33 行:生成时间段标签 [4],
  • 第 34–37 行:如果时间段内有预约,则显示摘要,
  • 第 36 行:显示预约客户的姓名,
  • 第 41–45 行:若该时段有预约,则显示 [删除] 按钮,
  • 第 46–50 行:若无预约,则显示 [预约] 按钮。

此视图主要由以下模型提供数据:


private AgendaMedecinJour agendaMedecinJour;

该模型在第24行填充了dataList字段。该字段是在从视图1切换到视图2时,由getAgenda方法创建的。

9.10. 删除预约

删除预约的步骤如下:

此操作涉及的视图如下:


<ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
              <p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.supprimer']}" icon="minus" update=":form2, :vueErreurs">
                <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
              </p:commandButton>
            </ui:fragment>
  • 第 2 行:[Delete] 按钮与 [Form].action 方法相关联(action 属性),
  • 第 3 行:当前选中的时段 ID 将发送至 [Form].idCreneauChoisi 模型,
  • 第 2 行:AJAX 调用将更新 form2view2 表单)的字段以及 vueErreurs 视图。可能出现两种情况:如果一切正常,将重新显示 vue2 视图;否则,将显示 vueErreurs 视图。

[action]方法如下:


  // action on RV
  public String action() {
    // search for the time slot in the calendar
    int i = 0;
    Boolean trouvé = false;
    while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneauChoisi) {
        trouvé = true;
      } else {
        i++;
      }
    }
    // have we found?
    if (!trouvé) {
      // it's weird - form2 is redisplayed
      setForms(false, true, false, false);
      return "pm:vue2";
    } else {
      creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    }
    // we found
    // according to desired action
    if (creneauChoisi.getRv() == null) {
      return reserver();
    } else {
      return supprimer();
    }
  }
 
  // reservation
  public String reserver() {
 ...
  }
 
  public String supprimer() {
    try {
      // deleting an appointment
      metier.supprimerRv(creneauChoisi.getRv());
      // updating the agenda
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // form2 is displayed
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
}
  • 第3至12行:我们查找ID与接收到的(第7行)相符的时间段,
  • 若未找到(这属于异常情况),则重新显示视图2(第16-17行),
  • 第 19 行:若找到,则存储对应的 [CreneauMedecinJour] 对象。该对象使我们能够访问待删除的预约,
  • 第 26 行:我们将其删除,
  • 第35–48行:如果删除成功(第42–43行),delete方法返回vue2视图;如果出现问题(第46–47行),则返回vueErrors视图。

9.11. 预约

预约操作遵循以下流程:

我们从 vue2 视图导航至 vue3 视图。此操作涉及的代码如下:


<ui:fragment rendered="#{creneauMedecinJour.rv==null}">
              <p:commandButton inline="true" action="#{form.action}" value="#{msg['form2.reserver']}" icon="plus" update=":vue3, :vueErreurs">
                <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneauChoisi}"/>
              </p:commandButton>
            </ui:fragment>
  • 第 2 行:[Book] 按钮关联了 [Form].action 方法(action 属性),因此其实现与 [Delete] 按钮相同。AJAX 调用会根据调用处理过程中是否出现错误,相应地更新 vue3 vueErrors 视图。
  • 第 3 行:与 [Delete] 按钮一样,时段 ID 会被传递给模型。

处理此操作的模型如下:


// action on RV
  public String action() {
    ...
    // according to desired action
    if (creneauChoisi.getRv() == null) {
      return reserver();
    } else {
      return supprimer();
    }
  }
 
  // reservation
  public String reserver() {
    try {
      // title form 3
      form3Titre = Messages.getMessage(null, "form3.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour),
                creneauChoisi.getCreneau().getHdebut(), creneauChoisi.getCreneau().getMdebut(), creneauChoisi.getCreneau().getHfin(), creneauChoisi.getCreneau().getMfin()}).getSummary();
      // form 3 is displayed
      setForms(false, false, true, false);
      return "pm:vue3";
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
  }
  • 第 2–10 行:该操作方法获取正在预订的 [CreneauMedecinJour] 对象的 creneauChoisi 引用,然后调用 reserver 方法,
  • 第16行:构建一条国际化消息。内容如下:

form3.titre={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}
form3.titre_detail={0} {1} {2}<br/>{3}<br/>{4,number,#00}h:{5,number,#00}-{6,number,#00}h:{7,number,#00}

这将是 Vue 3 视图的标题。与 Vue 2 视图类似,该标题包含 XML 内容,同时也包含用于显示时间段的格式化参数,

  • 第 19-20 行:显示 Vue 3 视图,
  • 第23-24行:若遇到问题,则显示 vueErrors 视图。

9.12. 确认预约

预约验证遵循以下步骤:

[1] 处为 Vue 3 视图,[2] 处为添加预约后的 Vue 2 视图。

Vue3 的代码 [vue3.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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">
 
  <!-- View 3 -->
  <pm:header title="#{msg['page.titre']}" swatch="b"/>
  <pm:content>
    <h:form id="form3">
      <p:commandButton inline="true" value=" " icon="back" action="#{form.showVue2}" update=":vue2"/>
      <div align="center">
        <h3><h:outputText value="#{form.form3Titre}" style="color: blue" escape="false"/></h3>
      </div>
      <pm:field>
        <h:outputLabel value="#{msg['form3.client']}" for="choixClient"/>
        <h:selectOneMenu id="choixClient" value="#{form.idClient}">
          <f:selectItems value="#{form.clients}" var="client" itemLabel="#{client.titre} #{client.prenom} #{client.nom}" itemValue="#{client.id}"/>
        </h:selectOneMenu>
      </pm:field>
      <div align="center">
        <p:commandButton inline="true" value="#{msg['form3.valider']}" action="#{form.validerRv}" update=":vue2, :vueErreurs" icon="check"/>
      </div>
    </h:form>
  </pm:content>
</html>
  • 第 16 行:生成视图标题 [3]。请注意 escape 属性的值,它允许在标题中解释 XHTML 字符,
  • 第 18–23 行:生成客户端下拉列表 [4],
  • 第 25 行:生成 [Validate] 按钮 [5]。该按钮关联了 [Form].validateRv 方法:

// rv validation
  public String validerRv() {
    try {
      // retrieve an instance of the chosen slot
      Creneau creneau = metier.getCreneauById(idCreneauChoisi);
      // we add the Rv
      metier.ajouterRv(jour, creneau, hClients.get(idClient));
      // updating the agenda
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // form2 is displayed
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      // error view
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
  }

该代码在第 01 版中已出现。请注意视图的显示方式:

  • 如果一切正常,则显示 vue2 视图(第 11–12 行),
  • 否则将显示 vueErreurs 视图(第 15–16 行)。

9.13. 取消预约

这对应于以下流程:

视图 [vue3.xhtml] 中的按钮 [1] 如下所示:


      <p:commandButton inline="true" value=" " icon="back" action="#{form.showVue2}" update=":vue2"/>

因此调用了 [Form].showVue2 方法。它仅用于显示 vue2


  public String showVue2() {
    // vue2
    setForms(false, true, false, false);
    return "pm:vue2?reverse=true";
}

9.14. 浏览日历

view2 中,您可以通过按钮在日历中导航:

上一天:

次日:

今天:

虽然上方的截图中未显示,但日历会自动更新,并显示新选定日期的日程安排。

[vue2.xhtml]文件中,这三个按钮的标签如下:


<pm:buttonGroup orientation="horizontal">
          <p:commandButton inline="true" icon="back" value=" " action="#{form.showVue1}" update=":vue1"/>
          <p:commandButton inline="true" icon="minus" value=" " action="#{form.getPreviousAgenda}" update=":form2"/>
          <p:commandButton inline="true" icon="home" value=" " action="#{form.getTodayAgenda}" update=":form2"/>
          <p:commandButton inline="true" icon="plus" value=" " action="#{form.getNextAgenda}" update=":form2"/>
        </pm:buttonGroup>

方法 [Form].getPreviousAgenda[Form].getNextAgenda [Form].today 已在示例 03 中介绍过。

9.15. 更改显示语言

通过首页上的按钮可以切换语言:

该按钮的代码如下:


      <p:button icon="gear" value=" "  href="#config" />

因此,它将跳转至 [2] 配置视图。[config.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:pm="http://primefaces.org/mobile"
      xmlns:ui="http://java.sun.com/jsf/facelets">
 
  <!-- View 1 -->
  <pm:header title="#{msg['page.titre']}" swatch="b">
    <f:facet name="left">
      <p:button icon="back" value=" " href="#vue1?reverse=true" />
    </f:facet>
  </pm:header>
  <pm:content>
    <h:form id="frmConfig">
      <div align="center">
        <h3><h:outputText value="#{msg['config.titre']}" style="color: blue"/></h3>
      </div>
      <pm:field>
        <h:outputLabel value="#{msg['config.langue']}" for="langue"/>
        <h:selectOneRadio id="langue" value="#{form.locale}">
          <f:selectItem itemLabel="#{msg['config.langue.francais']}" itemValue="fr"/>
          <f:selectItem itemLabel="#{msg['config.langue.anglais']}" itemValue="en" />
        </h:selectOneRadio>
      </pm:field>
      <p:commandButton value="#{msg['config.valider']}" action="#{form.configurer}" update=":vue1"/>
    </h:form>
  </pm:content>
</html>
  • 第 11 行:显示 [3],
  • 第 13 行:显示按钮 [4]。此按钮允许您返回 vue1 视图,
  • 第 17 行:视图的表单,
  • 第 19 行:显示视图标题 [5],
  • 第 21–27 行:显示单选按钮。所选单选按钮的值(itemValue)将提交至 [Form].locale 模型(第 23 行的 value 属性),
  • 第 28 行:显示 [提交] 按钮。AJAX 调用更新 vue1 视图(update 属性)。调用的方法是 [Form].configure

public String configurer(){
    // after configuration - redisplay view1
    redirect();
    return null;
  }
  
  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);
    }
  }

configure 方法(第 1 行)仅将移动浏览器重定向到应用程序的 URL。因此,将加载 [index.xhtml] 页面:


<f:view xmlns="http://www.w3.org/1999/xhtml"
        xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html"
        xmlns:ui="http://java.sun.com/jsf/facelets"
        xmlns:p="http://primefaces.org/ui"
        xmlns:pm="http://primefaces.org/mobile"
        contentType="text/html"
        locale="#{form.locale}">
 
  <pm:page title="#{msg['page.titre']}">
    <pm:view id="vue1">
      ...
    </pm:view>
    ...
  </pm:page>    
</f:view>
  • 第 8 行:视图将使用刚刚更改的语言(locale 属性),并显示 view1(第 11 行)。

9.16. 结论

让我们回顾一下刚刚构建的应用程序的架构:

为了过渡到移动界面,需要重写 XHTML 页面。另一方面,模型几乎没有变化。底层 [业务]、[DAO]、[JPA] 层则完全没有改变。

9.17. Eclipse 测试

与之前版本的示例应用程序一样,我们将演示如何使用 Eclipse 测试此 05 版。首先,我们将示例 05 [1] 中的 Maven 项目导入到 Eclipse 中:

  • [mv-rdvmedecins-ejb-dao-jpa]:[DAO] 和 [JPA] 层,
  • [mv-rdvmedecins-ejb-metier]:[业务]层,
  • [mv-rdvmedecins-pfmobile]:由 PrimeFaces Mobile 实现的 [Web] 层,
  • [mv-rdvmedecins-pfmobile-app]:企业项目 [mv-rdvmedecins-pfmobile-app-ear] 的父项目。导入父项目时,子项目会自动被导入,
  • 在 [2] 中,运行企业项目 [mv-rdvmedecins-pfmobile-app-ear],
  • 在 [3] 中,选择 Glassfish 服务器,
  • 在 [4] 中,于 [Servers] 选项卡下,应用程序已部署完成。它不会自动运行。您必须在浏览器或移动模拟器 [5] 中输入其 URL [http://localhost:8080/mv-rdvmedecins-pfmobile/]: