Skip to content

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

回顾为 Glassfish 服务器开发的示例应用 01 JSF / EJB 的结构:

除了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中[métier]层的EJB项目,
  • [mv-rdvmedecins-pfmobile]:[web] 层的项目 / Primefaces 移动版 – 新增,
  • [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 的作用域为 application,[Form] Bean 的作用域为 session。 类 [Erreur] 封装了一个错误,
  • 在 [3] 中,用于国际化的消息文件,
  • 在 [4] 中,依赖项。Web 项目依赖于 [DAO] 层中的 EJB 项目, [métier] 层的 EJB 项目,以及 [web] 层的 Primefaces Mobile。

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.titre=Les M\u00e9decins Associ\u00e9s
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy

# 视图1
vue1.header=Les M\u00e9decins Associ\u00e9s - R\u00e9servations

# 表单 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

# 表单 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'hui

# 表单 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.titre=Une erreur s'est produite.

# 配置
config.retour=Retour
config.titre=Configuration
config.langue=Langue
config.langue.francais=Fran\u00e7ais
config.langue.anglais=Anglais
config.valider=Valider

#异常
exception.titre=Application indisponible. Veuillez recommencer ult\u00e9rieurement.

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


# 页面
page.titre=The Associated Doctors
format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy

# 视图1
vue1.header=The Associated Doctors - Reservations

# 表单 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

# 表单 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

# 表单 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.titre=Some error happened

# 配置
config.retour=Back
config.titre=Configuration
config.langue=Language
config.langue.francais=French
config.langue.anglais=English
config.valider=Validate

#异常
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 行:该页面包含五个视图:视图 1 第 11 行,视图 2 第 19 行,视图 3 第 24 行,vueErreurs 第 29 行,配置 第 34 行。在某个时刻,仅其中一个视图可见。应用程序启动时,显示的是视图 1。 在此,我们遇到了以下难题:如果应用程序初始化成功,则应显示 [vue1.xhtml];否则,应显示 [vueErreurs.xhtml]。 我们通过让视图 vue1 的内容由模型管理来解决了这个问题,该模型为布尔值 [Form].form1rendered(第 12 行)和 [Form].erreurInit(第15行)布尔值,从而确定视图1(第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. Application Bean

Bean [Application.java] 是 application 作用域下的 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 {

  // 业务层
  @EJB
  private IMetierLocal metier;

  public Application() {
  }
  
  // 获取器

  public IMetierLocal getMetier() {
    return metier;
  }
  
}
  • 第 8 行:将 Bean 命名为 application,
  • 第 9 行:其作用域为应用程序
  • 第 13-14 行:应用服务器上的容器 EJB 将向其注入 [métier] 层的本地接口引用。回顾一下应用程序的架构:

应用程序 PFM、EJB 和 [Metier] 将在同一个 JVM(Java 虚拟机)中运行。 因此,[PFM] 层将使用 EJB 的本地接口。仅此而已。 [Application] Bean 本身不包含其他内容。若要访问 [métier] 层,其他 Bean 将通过该 Bean 进行调用。

9.7.2. [Erreur] Bean

[Erreur] 类如下:


package beans;

public class Erreur {
  
  public Erreur() {
  }
  
  // 字段
  private String classe;
  private String message;

  // 构造函数
  public Erreur(String classe, String message){
    this.setClasse(classe);
    this.message=message;
  }
  
  // 获取器和设置器
...  
}
  • 第 9 行:若抛出异常,则为异常类的名称;
  • 第10行:错误消息。

9.7.3. Bean [Form]

其代码如下:


package beans;

...

@Named(value = "form")
@SessionScoped
public class Form implements Serializable {

  public Form() {
  }
  // 应用程序 Bean
  @Inject
  private Application application;
  private IMetierLocal metier;
  // 会话缓存
  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 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");
    // 起初没有错误
    erreurInit = false;
    // 日期格式化
    dateFormatter = new SimpleDateFormat(Messages.getMessage(null, "format.date", null).getSummary());
    dateFormatter.setLenient(false);
    // 当前日期
    strJour = dateFormatter.format(jour);
    // 获取业务层
    metier = application.getMetier();
    // 将医生和客户数据缓存
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // 记录错误
      erreurInit = true;
      prepareVueErreur(th);
      return;
    }
    // 验证列表
    if (medecins.size() == 0) {
      // 记录错误
      erreurInit = true;
      erreurs = new ArrayList<Erreur>();
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // 记录错误
      erreurInit = true;
      erreurs = new ArrayList<Erreur>();
      erreurs.add(new Erreur("", "La liste des clients est vide"));
    }
    // 错误?
    if (erreurInit) {
      // 显示错误视图
      setForms(false, false, false, true);
      return;
    }

    // 字典
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
    }
    // 视图 1
    setForms(true, false, false, false);
  }

  // 视图显示
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.form3Rendered = form3Rendered;
    this.erreurRendered = erreurRendered;
  }

  // 准备vueErreur
  private void prepareVueErreur(Throwable th) {
    // 生成错误列表
    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()));
    }
// 显示错误视图
    setForms(false, false, false, true);
  }

  // 获取器和设置器
  ..
}
  • 第5-7行:类[Form]是一个名为form且作用域为session的Bean。需要提醒的是,此时该类必须可序列化,
  • 第12-13行:表单Bean持有应用程序Bean的引用。该引用将由应用程序运行的Servlet容器注入(存在@Inject注解)。
  • 第 24-27 行:控制视图 vue1(第 24 行)、vue2(第 25 行)、vue3(第 26 行)和 vueErreurs(第 27 行)的显示,
  • 第43-44行:类实例化后立即执行init方法(存在@PostConstruct注解),
  • 第49-50行:处理日期格式。PrimeFaces Mobile不提供日历功能。此外,JSF验证器无法在PFM页面中使用。因此我们需要手动处理日程表的日期输入,
  • 第49行:设定日期格式。该格式取自国际化文件:

format.date=dd/MM/yyyy
format.date_detail=dd/MM/yyyy
  • 第50行:指定日期处理时不应采用“laxiste”格式。若不进行此设置,输入“32/12/2011”这类错误格式将被视为有效日期“01/01/2012”,
  • 第54行:从Bean [Application]中获取[métier]层的引用,
  • 第57-58行:向[métier]层请求医生和客户列表,
  • 第66-91行:若一切顺利,则构建医生和客户的字典。它们按编号进行索引。随后,将显示视图[vue1.xhtml](第93行),
  • 第59行:若发生错误,则构建页面[vueErreurs.xhtml]的模板。该模板即第35行中的错误列表,
  • 第105-115行:方法[prepareVueErreur]构建待显示的错误列表。随后,页面[index.xhtml]显示视图[vueErreurs.xhtml](第114行)。

页面 [erreur.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">

  <!-- 错误视图 -->
  <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 行的模板 #{erreur.classe} 生成,
  • 文本 [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">

  <!-- 视图 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(view1中的表单)、view2以及vueErreurs。实际上,如果日期无效,则应更新form1;如果日期正确,则应更新view2。 最后,如果发生异常(例如数据库连接中断),则应显示 vueErreurs。可能会有人想用 vue1 代替 form1(更新整个视图)。但在这种情况下,应用程序会出现故障。

该视图基于以下模型:


@Named(value = "form")
@SessionScoped
public class Form implements Serializable {

  public Form() {
  }

  // 应用程序 Bean
  @Inject
  private Application application;
  private IMetierLocal metier;
  // 会话缓存
  private List<Medecin> medecins;
  private List<Client> clients;
  // 模型
  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;
  
    // 医生列表
  public List<Medecin> getMedecins() {
    return medecins;
  }

  // 日程表
  public String getAgenda() {
...
    }
  }
  • 第16行的字段负责读取和写入页面第23行列表中的值。在页面初次显示时,它会将下拉列表中选中的值固定下来,
  • 第27-29行的方法生成医生下拉列表的选项(视图第24行)。每个生成的选项将以(itemLabel)作为标签,显示医生的头衔姓氏和名字,并以(itemValue)作为值,表示医生的ID
  • 第18行的字段通过读写方式为页面第29行的输入字段提供数据,
  • 第32-34行:方法getAgenda负责处理页面第35行按钮[Agenda]的点击事件。其代码如下:

// 日程表
  public String getAgenda() {
    try {
      // 查看日期
      jour = dateFormatter.parse(strJour);
      // 无错误
      erreurDate=false;
      msgErreurDate = "";
      // 创建日程表
      return getAgenda(jour);
    } catch (ParseException ex) {
      // 错误信息
      erreurDate=true;
      msgErreurDate = Messages.getMessage(null, "form1.date.invalide", null).getSummary();
      // 视图1      
      setForms(true, false, false, false);
      return "pm:vue1";
    }
  }
  • 该方法首先验证用户输入的日期是否有效,
  • 第 5 行:日期格式为 parsée,该格式由方法 init 在模型实例化时初始化,
  • 第11行:若发生异常,则设置错误(第13行),构建国际化错误消息(第14行),准备视图 vue1(第16行)并显示视图 vue1(第17行),
  • 第 10 行:如果日期正确,则执行以下方法:

  // 日程表
  public String getAgenda(Date jour) {
    // 目前尚未选择时段
    creneauChoisi = null;
    try {
      // 获取所选医生
      medecin = hMedecins.get(idMedecin);
      // 表单标题 2
      form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
      // 某一天的医生日程表
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // 显示视图 2
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      System.out.println(th);
      // 错误视图
      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">

  <!-- 视图 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行:若该时段有预约,则显示按钮[Supprimer],
  • 第46-50行:若该时段内无预约,则显示按钮 [Réserver]。

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


private AgendaMedecinJour agendaMedecinJour;

该字段为第24行的dataList提供数据。该字段由方法getAgenda构建,当从视图1切换到视图2时生成。

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 行:按钮 [Supprimer] 关联了方法 [Form].actionaction 属性),
  • 第 3 行:当前所处时段的 ID 将被发送至 [Form].idCreneauChoisi 模型,
  • 第2行:调用AJAX将更新form2视图2表单)区域以及视图vueErreurs。 实际上存在两种情况:如果一切正常,将重新显示视图2;否则将显示视图vueErreurs

方法 [action] 如下:


  // 对 RV 执行操作
  public String action() {
    // 在日程表中搜索时段
    int i = 0;
    Boolean trouvé = false;
    while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneauChoisi) {
        trouvé = true;
      } else {
        i++;
      }
    }
    // 找到了吗?
    if (!trouvé) {
      // 真奇怪——重新显示 form2
      setForms(false, true, false, false);
      return "pm:vue2";
    } else {
      creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    }
    // 找到了
    // 根据所需操作
    if (creneauChoisi.getRv() == null) {
      return reserver();
    } else {
      return supprimer();
    }
  }

  // 预约
  public String reserver() {
 ...
  }

  public String supprimer() {
    try {
      // 删除预约
      metier.supprimerRv(creneauChoisi.getRv());
      // 更新日程表
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // 显示表单2
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      // 显示错误
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
}
  • 第3-12行:根据收到的ID(第7行)查找对应的时间段,
  • 如果找不到它(这不正常),则重新显示视图2(第16-17行),
  • 第19行:如果找到了,则保存对应的对象[CreneauMedecinJour]。正是它让我们能够访问要删除的预约,
  • 第26行:将其删除,
  • 第35-48行:如果删除操作成功(第42-43行),则“删除”方法返回视图 vue2;如果出现问题(第46-47行),则返回视图 vueErreurs

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行:按钮 [Réserver] 关联的方法为 [Form].action(属性 action),因此与按钮 [Supprimer] 的关联方法相同。 调用 AJAX 会根据调用处理过程中是否出现错误,分别更新 vue3 vueErreurs 视图。
  • 第 3 行:与按钮 [Supprimer] 一样,时段的 ID 被传递给了模型。

处理此操作的模板如下:


// 对 RV 执行操作
  public String action() {
    ...
    // 根据所需操作
    if (creneauChoisi.getRv() == null) {
      return reserver();
    } else {
      return supprimer();
    }
  }

  // 预订
  public String reserver() {
    try {
      // 表单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();
      // 显示表单 3
      setForms(false, false, true, false);
      return "pm:vue3";
    } catch (Throwable th) {
      // 错误视图
      prepareVueErreur(th);
      return "pm:vueErreurs";
    }
  }
  • 第 2-10 行:方法 action 将确定待预订对象 [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}

这将成为视图 vue3 的标题。与视图 vue2 一样,该标题包含 XML。它还包含用于显示时段时间表的格式化参数,

  • 第19-20行:显示视图 vue3
  • 第23-24行:若遇到问题,则显示视图vueErreurs

9.12. 确认预约

确认预约的流程如下:

在 [1] 中显示视图 vue3,在 [2] 中显示视图 vue2(添加预约后)。

视图3的代码[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">

  <!-- 视图 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行:生成按钮 [Valider] [5]。方法 [Form].validerRv 与该按钮相关联:

// 预约确认
  public String validerRv() {
    try {
      // 获取所选时段的实例
      Creneau creneau = metier.getCreneauById(idCreneauChoisi);
      // 添加预约
      metier.ajouterRv(jour, creneau, hClients.get(idClient));
      // 更新日程表
      agendaMedecinJour = metier.getAgendaMedecinJour(medecin, jour);
      // 显示 form2
      setForms(false, true, false, false);
      return "pm:vue2";
    } catch (Throwable th) {
      // 错误视图
      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。该方法仅显示视图2


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

9.14. 日历导航

视图2中,通过按钮可在日历中进行导航:

前一天:

次日:

今天:

虽然未在上述截图中显示,但日程表已更新,并显示了所选新日期的约会。

在 [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">

  <!-- 视图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行:显示按钮[Valider]。调用AJAX会更新视图vue1update属性)。被调用的方法是[Form].configurer

public String configurer(){
    // 配置完成后 - 重新显示视图1
    redirect();
    return null;
  }
  
  private void redirect() {
    // 将客户端重定向至 Servlet
    ExternalContext ctx = FacesContext.getCurrentInstance().getExternalContext();
    try {
      ctx.redirect(ctx.getRequestContextPath());
    } catch (IOException ex) {
      Logger.getLogger(Form.class.getName()).log(Level.SEVERE, null, ex);
    }
  }

configurer 方法(第 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 属性),并显示 vue1(第 11 行)。

9.16. 结论

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

为了适配移动端界面,需要重写页面 XHTML。 而模型部分则几乎没有变化。底层模块 [métier]、[DAO]、[JPA] 则完全未作改动。

9.17. Eclipse 测试

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

  • [mv-rdvmedecins-ejb-dao-jpa]:包含 [DAO] 和 [JPA] 两个层,
  • [mv-rdvmedecins-ejb-metier]:[métier]层,
  • [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] 选项卡中,应用程序已部署。它不会自动运行。 需在浏览器或移动模拟器中手动启动该应用: