Skip to content

6. Beispielanwendung-03: rdvmedecins-pf-ejb

Sehen wir uns die Struktur der für den GlassFish-Server entwickelten Beispielanwendung an:

Wir nehmen keine Änderungen an dieser Architektur vor, mit Ausnahme der Webschicht, die hier mithilfe von JSF und PrimeFaces implementiert wird.

6.1. Das NetBeans-Projekt

Oben sind die [Business]- und [DAO]-Schichten diejenigen aus Beispiel 01 JSF / EJB / Glassfish. Wir verwenden sie wieder.

  
  • [mv-rdvmedecins-ejb-dao-jpa]: EJB-Projekt für die [DAO]- und [JPA]-Schichten aus Beispiel 01,
  • [mv-rdvmedecins-ejb-metier]: EJB-Projekt für die [Business]-Schicht aus Beispiel 01,
  • [mv-rdvmedecins-pf]: Projekt für die [Web]-Schicht / Primefaces – neu,
  • [mv-rdvmedecins-app-ear]: Unternehmensprojekt zur Bereitstellung der Anwendung auf dem GlassFish-Server – neu.

6.2. Das Unternehmensprojekt

Das Unternehmensprojekt dient ausschließlich der Bereitstellung der drei Module [mv-rdvmedecins-ejb-dao-jpa], [mv-rdvmedecins-ejb-business], [mv-rdvmedecins-pf] auf dem GlassFish-Server. Das NetBeans-Projekt sieht wie folgt aus:

Das Projekt dient ausschließlich diesen drei Abhängigkeiten [1], die in der Datei [pom.xml] wie folgt definiert sind:


<?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>
  • Zeilen 10–13: das Maven-Artefakt für das Unternehmensprojekt,
  • Zeilen 18–37: die drei Abhängigkeiten des Projekts. Beachten Sie deren Typen (Zeilen 23, 29, 35).

Um die Webanwendung auszuführen, müssen Sie dieses Unternehmensprojekt ausführen.

6.3. Das PrimeFaces-Webprojekt

Das PrimeFaces-Webprojekt sieht wie folgt aus:

  • in [1] die Seiten des Projekts. Die Seite [index.xhtml] ist die einzige Seite des Projekts. Sie enthält drei Fragmente: [form1.xhtml], [form2.xhtml] und [error.xhtml]. Die anderen Seiten dienen ausschließlich der Formatierung.
  • in [2] die Java-Beans. Die [Application]-Bean hat Anwendungsbereich, und die [Form]-Bean hat Sitzungsbereich. Die [Error]-Klasse kapselt einen Fehler. Die [MyDataModel]-Klasse dient als Modell für ein PrimeFaces-<dataTable>-Tag,
  • in [3] die Nachrichtendateien für die Internationalisierung,
  • in [4] die Abhängigkeiten. Das Webprojekt hängt vom EJB-Projekt für die [DAO]-Schicht, vom EJB-Projekt für die [Business]-Schicht und von PrimeFaces für die [Web]-Schicht ab.

6.4. Projektkonfiguration

Die Projektkonfiguration entspricht derjenigen der PrimeFaces- oder JSF-Projekte, die wir bereits behandelt haben. Wir listen die Konfigurationsdateien auf, ohne sie erneut zu erläutern.

 

[web.xml]: Konfiguriert die Webanwendung.


<?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>

Beachten Sie, dass in Zeile 30 die Seite [index.xhtml] die Startseite der Anwendung ist.

[faces-config.xml]: Konfiguriert die JSF-Anwendung


<?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]: leer, aber für die Annotation @Named erforderlich


<?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]: das Stylesheet der Anwendung


.col1{
   background-color: #ccccff
}
 
.col2{
   background-color: #ffcccc
}

Die PrimeFaces-Bibliothek enthält eigene Stylesheets. Das obige Stylesheet wird nur für die Seite verwendet, die im Falle einer Ausnahme angezeigt wird – eine Seite, die nicht von der Anwendung verwaltet wird. Die Seite [exception.xhtml] wird dann angezeigt.

[messages_fr.properties]: die französische Sprachdatei


# 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]: die englische Meldungsdatei


# 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. Die Seitenvorlage [layout.xhtml]

Die Vorlage [layout.xhtml] sieht wie folgt aus:


<?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>

Der einzige variable Teil dieser Vorlage ist der Bereich in den Zeilen 28–30. Dieser Bereich befindet sich in der ID :form:content (Zeile 27). Behalten Sie dies im Hinterkopf. AJAX-Aufrufe, die diesen Bereich aktualisieren, haben das Attribut update=":form:content". Außerdem beginnt das Formular in Zeile 15. Daher wird das in den Zeilen 28–30 eingefügte Fragment in dieses Formular eingefügt.

Diese Vorlage erzeugt die folgende Ausgabe:

Der dynamische Teil der Seite wird in den oben eingerahmten Bereich eingefügt.

6.6. Die Seite [index.xhtml]

Das Projekt zeigt immer dieselbe Seite an, nämlich die folgende [index.xhtml]-Seite:


<?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>
  • Zeilen 8–9: Dieses XHTML-Fragment wird in den dynamischen Bereich der Vorlage [layout.xhtml] eingefügt,
  • die Seite besteht aus drei Unterfragmenten:
  • [form1.xhtml], Zeilen 10–12;
  • [form2.xhtml], Zeilen 13–15;
  • [error.xhtml], Zeilen 16–18.

Das Vorhandensein dieser Fragmente in [index.xhtml] wird durch Boolesche Werte in der mit der Seite verknüpften Vorlage [Form.java] gesteuert. Durch Anpassen dieser Booleschen Werte ändert sich daher die gerenderte Seite.

Das Fragment [form1.xhtml] wird wie folgt dargestellt:

Das Fragment [form2.xhtml] wird wie folgt dargestellt:

Das Fragment [erreur.xhtml] wird wie folgt dargestellt:

6.7. Die Beans des Projekts

Die Klasse im [utils]-Paket wurde bereits vorgestellt: Die [Messages]-Klasse erleichtert die Internationalisierung der Meldungen einer Anwendung. Sie wurde in Abschnitt 2.8.5.7 behandelt.

6.7.1. Die Application-Bean

Die Bean [Application.java] ist eine Bean im Anwendungsbereich. Zur Erinnerung: Diese Art von Bean wird verwendet, um schreibgeschützte Daten zu speichern, die allen Benutzern der Anwendung zur Verfügung stehen. Diese Bean sieht wie folgt aus:


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;
  }
  
}
  • Zeile 8: Wir geben der Bean den Namen „application“,
  • Zeile 9: Sie hat Anwendungsbereich,
  • Zeilen 13–14: Eine Referenz auf die lokale Schnittstelle der [Business]-Schicht wird vom EJB-Container des Anwendungsservers in sie injiziert. Sehen wir uns die Anwendungsarchitektur noch einmal an:

Die JSF-Anwendung und das [Business]-EJB laufen in derselben JVM (Java Virtual Machine). Daher nutzt die [JSF]-Schicht die lokale Schnittstelle des EJB. Das ist alles. Die [Application]-Bean enthält nichts weiter. Um auf die [Business]-Schicht zuzugreifen, beziehen die anderen Beans diese aus dieser Bean.

6.7.2. Die [Error]-Bean

Die [Error]-Klasse sieht wie folgt aus:

  1. Paket 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
...  
}
  • Zeile 9: der Name einer Ausnahmeklasse, falls eine Ausnahme ausgelöst wurde,
  • Zeile 10: eine Fehlermeldung.

6.7.3. Die [Form]-Bean

Der Code lautet wie folgt:


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
  ...
}
  • Zeilen 6–8: Die Klasse [Form] ist ein Bean namens „form“ mit Session-Gültigkeitsbereich. Beachten Sie, dass die Klasse daher serialisierbar sein muss,
  • Zeilen 14–15: Die Form-Bean verfügt über eine Referenz auf die Anwendungs-Bean. Diese Referenz wird vom Servlet-Container, in dem die Anwendung läuft, injiziert (Vorhandensein der Annotation @Inject).
  • Zeilen 17–44: Die Seitenvorlagen [form1.xhtml, form2.xhtml, error.xhtml]. Die Anzeige dieser Seiten wird durch die booleschen Werte in den Zeilen 27–29 gesteuert. Beachten Sie, dass standardmäßig die Seite [form1.xhtml] gerendert wird (Zeile 27),
  • Zeilen 46–47: Die `init`-Methode wird unmittelbar nach der Instanziierung der Klasse ausgeführt (aufgrund des Vorhandenseins der `@PostConstruct`-Annotation),
  • Zeilen 50–51: Die [business]-Schicht wird nach der Liste der Ärzte und Kunden abgefragt,
  • Zeilen 59–65: Wenn alles geklappt hat, werden die Wörterbücher für Ärzte und Kunden erstellt. Sie sind nach ihrer Nummer indiziert. Als Nächstes wird die Seite [form1.xhtml] angezeigt (Zeile 27),
  • Zeile 54: Im Falle eines Fehlers wird die Seitenvorlage [error.xhtml] erstellt. Diese Vorlage enthält die Fehlerliste aus Zeile 36,
  • Zeilen 78–88: Die Methode [prepareVueErreur] erstellt die Liste der anzuzeigenden Fehler. Die Seite [index.xhtml] zeigt dann die Fragmente [form1.xhtml] und [erreur.xhtml] an (Zeile 87).

Die Seite [error.xhtml] sieht wie folgt aus:


<?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>

Es verwendet ein <p:dataTable>-Tag (Zeilen 12–28), um die Liste der Fehler anzuzeigen. Dies führt zu einer Fehlerseite, die in etwa wie folgt aussieht:

Wir werden nun die verschiedenen Phasen des Lebenszyklus der Anwendung definieren. Für jede Benutzeraktion werden wir die entsprechenden Ansichten und Ereignisbehandler untersuchen.

6.8. Anzeigen der Startseite

Wenn alles gut geht, wird als erste Seite [form1.xhtml] angezeigt. Dies führt zu folgender Ansicht:

Die Seite [form1.xhtml] sieht wie folgt aus:


<?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>

Die im Screenshot hervorgehobene Symbolleiste ist die PrimeFaces-Toolbar-Komponente. Sie ist in den Zeilen 8–14 definiert. Sie enthält zwei Gruppen von Komponenten, die jeweils durch ein <toolbarGroup>-Tag in den Zeilen 9–11 und 12–14 definiert sind. Eine der Gruppen ist linksbündig in der Symbolleiste ausgerichtet (Zeile 9), die andere rechtsbündig (Zeile 12).

Sehen wir uns einige Komponenten in der linken Gruppe an:


<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>
...
  • Zeilen 4–7: das Dropdown-Menü „Ärzte“, dem ein Effekt hinzugefügt wurde (effect="fade"),
  • Zeile 6: Ein AJAX-Verhalten. Bei einer Änderung im Dropdown-Menü wird die Methode [Form].hideAgenda (listener="#{form.hideAgenda}") ausgeführt und der dynamische Bereich :form:content (update=":form:content") aktualisiert.
  • Zeile 8: Fügt ein Trennzeichen in die Symbolleiste ein.
  • Zeilen 10–12: das Eingabefeld für das Datum. Hier wird der PrimeFaces-Kalender verwendet. Das Eingabefeld ist schreibgeschützt (readOnlyInputText="true"),
  • Zeile 11: ein AJAX-Verhalten. Wenn sich das Datum ändert, wird die Methode [Form].hideAgenda ausgeführt und das dynamische Feld :form:content aktualisiert,
  • Zeile 14: eine Schaltfläche. Ein Klick darauf löst einen AJAX-Aufruf der Methode [Form].getAgenda() aus; das Modell wird daraufhin geändert, und die Serverantwort wird verwendet, um das dynamische Feld :form:content zu aktualisieren,
  • Zeile 15: Mit dem <tooltip>-Tag können Sie einer Komponente einen Tooltip zuweisen. Die ID der Komponente wird durch das for-Attribut des Tooltips angegeben. Hier bezieht sich (for="resa-agenda") auf die Schaltfläche in Zeile 14:

Diese Seite basiert auf der folgenden Vorlage:


@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() {
    ...
  }
  • Das Feld in Zeile 12 liest den Wert der Liste in Zeile 4 der Seite und schreibt ihn dort. Beim ersten Laden der Seite wird der in der Kombinationsfeld ausgewählte Wert gesetzt. Beim ersten Laden ist idMedecin gleich null, daher wird der erste Arzt ausgewählt.
  • Die Methode in den Zeilen 16–18 generiert die Einträge für das Dropdown-Menü „Ärzte“ (Zeile 5 der Seite). Jeder generierte Eintrag enthält als Bezeichnung (itemLabel) den Titel, den Nachnamen und den Vornamen des Arztes und als Wert (itemValue) die ID des Arztes.
  • Das Feld in Zeile 13 bietet Lese-/Schreibzugriff auf das Eingabefeld in Zeile 10 der Seite. Bei der ersten Anzeige wird das aktuelle Datum angezeigt,
  • Zeilen 26–28: Die Methode getAgenda verarbeitet den Klick auf die Schaltfläche [Agenda] in Zeile 14 der Seite. Sie ist fast identisch mit der JSF-Version:

  // 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;
}

Wir werden diesen Code nicht kommentieren. Das wurde bereits getan.

6.9. Anzeige des Terminkalenders eines Arztes

6.9.1. Übersicht über den Terminplan

Hier ist der folgende Anwendungsfall:

  • In [1] wählen Sie einen Arzt [1] und einen Tag [2] aus und fordern dann [3] den Terminplan des Arztes für den ausgewählten Tag an;
  • in [4] wird dieser unterhalb der Symbolleiste angezeigt.

Der Code für die Seite [form2.xhtml] lautet wie folgt:


<?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>
  • Zeilen 16–26: Das Hauptelement der Seite ist die <dataTable>, die den Terminplan des Arztes anzeigt,
  • Zeilen 12–14: Wir verwenden ein Kontextmenü, um einen Termin hinzuzufügen oder zu löschen:
 
  • Zeilen 29–32: Wenn der Benutzer einen Termin löschen möchte, wird ein Bestätigungsdialogfeld angezeigt:
 
  • Zeilen 35–37: Zur Anzeige eines Fehlers wird ein Dialogfeld verwendet:
 
  • Zeilen 40–43: Wir müssen etwas JavaScript hinzufügen.

6.9.2. Die Terminübersicht

Hier behandeln wir das Datentabellenmodell, wie es in Abschnitt 5.15, Seite 327, beschrieben ist.

Betrachten wir das Hauptelement der Seite, die Tabelle, in der der Kalender angezeigt wird:


<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>

Das Ergebnis sieht wie folgt aus:

Dies ist eine zweispaltige Tabelle (Zeilen 4–6 und 8–10), die durch die Quelle [Form].getMyDataModel() (value="#{form.myDataModel}") gefüllt wird. Es kann jeweils nur eine Zeile ausgewählt werden (selectionMode="single"). Bei jedem POST-Aufruf wird [Form].creneauChoisi (selection="#{form.creneauChoisi}") ein Verweis auf das ausgewählte Element zugewiesen.

Zur Erinnerung: Die Methode getAgenda hat das folgende Feld im Modell initialisiert:


 
// modèle
private AgendaMedecinJour agendaMedecinJour;

Das Tabellenmodell wird durch Aufruf der folgenden Methode [Form].getMyDataModel abgerufen (Attribut „value“ des Tags <dataTable>):


  // the dataTable model
  public MyDataModel getMyDataModel() {
    return new MyDataModel(agendaMedecinJour.getCreneauxMedecinJour());
}

Betrachten wir die Klasse [MyDataModel], die als Modell für das <p:dataTable>-Tag dient:


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;
  }
}
  • Zeile 7: Die Klasse [MyDataModel] ist das Modell für das <p:dataTable>-Tag. Der Zweck dieser Klasse besteht darin, das gesendete rowkey-Element mit dem dieser Zeile zugeordneten Element zu verknüpfen.
  • Zeile 7: Die Klasse implementiert die Schnittstelle [SelectableDataModel] über die Klasse [ArrayDataModel]. Das bedeutet, dass der Konstruktorparameter ein Array ist. Dieses Array füllt das <dataTable>-Tag. Hier wird jede Zeile des Arrays einem Element vom Typ [CreneauMedecinJour] zugeordnet,
  • Zeilen 13–15: Der Konstruktor übergibt seinen Parameter an seine übergeordnete Klasse,
  • Zeilen 18–20: Jede Zeile des Arrays entspricht einem Zeitfenster und wird durch die ID des Zeitfensters identifiziert (Zeile 19). Es ist diese ID, die an den Server gesendet wird,
  • Zeile 23: Der Code, der serverseitig ausgeführt wird, wenn eine Zeitfenster-ID übermittelt wird. Der Zweck dieser Methode besteht darin, die Referenz auf das mit dieser ID verknüpfte [CreneauMedecinJour]-Objekt zurückzugeben. Diese Referenz wird dem Ziel des Attributs „selection“ des <dataTable>-Tags zugewiesen:

<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">

Das Feld [Form].creneauChoisi enthält daher die Referenz des Objekts [CreneauMedecinJour], das Sie hinzufügen oder löschen möchten.

6.9.3. Die Spalte „Zeitfenster“

Die Spalte „Zeitfenster“ wird mit dem folgenden Code generiert:


<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>
  • Zeilen 5–7: die Spaltenüberschrift,
  • Zeilen 8–15: das aktuelle Spaltenelement. Beachten Sie Zeile 9, wo das <h:outputFormat>-Tag verwendet wird, um die anzuzeigenden Elemente zu formatieren. Der Parameter „value“ gibt die anzuzeigende Zeichenfolge an. Die Notation {i,type,format} bezieht sich auf den Parameter mit der Nummer i, den Typ dieses Parameters und dessen Format. Hier gibt es 4 Parameter, die von 0 bis 3 nummeriert sind; ihr Typ ist numerisch, und sie werden mit zwei Ziffern angezeigt,
  • Zeilen 10–13: die vier Parameter, die vom <h:outputFormat>-Tag erwartet werden.

6.9.4. Die Spalte „Kunde“

Die Kundenspalte wird mit dem folgenden Code generiert:


<!-- 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>
  • Zeilen 8–10: die Spaltenüberschrift,
  • Zeilen 11–13: das aktuelle Element, wenn für den Zeitblock ein Termin vorliegt. In diesem Fall zeigen wir den Titel, den Vornamen und den Nachnamen des Kunden an, für den dieser Termin vereinbart wurde,
  • Zeilen 14–16: ein weiteres Fragment, auf das wir später zurückkommen werden.

6.10. Einen Termin löschen

Das Löschen eines Termins erfolgt in folgender Reihenfolge:

Die bei dieser Aktion verwendete Ansicht sieht wie folgt aus:


<!-- 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>  
  • Zeilen 2–5: Ein Kontextmenü, das mit dem Datenarray (für das Attribut) verknüpft ist. Es enthält zwei Optionen [1]:
  • Zeile 4: Die Option [Löschen] löst die Anzeige des Dialogfelds [2] aus den Zeilen 13–20 aus,
  • Zeile 15: Ein Klick auf [Ja] löst die Ausführung von [Form.action] aus, wodurch der Termin gelöscht wird. Normalerweise sollte das Kontextmenü die Option [Löschen] nicht anbieten, wenn das ausgewählte Element keinen Termin enthält, und die Option [Buchen] nicht, wenn das ausgewählte Element einen Termin enthält. Es ist uns nicht gelungen, das Kontextmenü so subtil zu gestalten. Es funktioniert zwar für das erste ausgewählte Element, doch dann haben wir festgestellt, dass das Kontextmenü die für diese erste Auswahl festgelegte Konfiguration beibehält. Dadurch wird es fehlerhaft. Deshalb haben wir beide Optionen beibehalten und beschlossen, dem Benutzer eine Rückmeldung zu geben, wenn er ein Element ohne Termin löscht,
  • Zeile 16: Das Attribut `oncomplete`, mit dem Sie JavaScript-Code definieren können, der nach Abschluss der AJAX-Anfrage ausgeführt werden soll. In diesem Fall lautet der Code wie folgt:

<!-- 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> 
  • Zeile 10: Der JavaScript-Code prüft, ob das Wörterbuch „args“ ein „error“-Attribut enthält. Ist dies der Fall, wird das Dialogfeld aus Zeile 2 (Attribut „widgetVar“) angezeigt. Dieses Dialogfeld zeigt die Vorlage [Form].msgError an.

Sehen wir uns den Code an, der zur Bearbeitung des Löschens eines Termins ausgeführt wird:


    <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>  
  • Zeile 2: Die Methode [Form].action wird ausgeführt,
  • Zeile 4: Bevor sie ausgeführt wird, wurde das Feld „action“ auf „delete“ gesetzt.

Die [action]-Methode sieht wie folgt aus:


// 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;
}
  • Zeile 4: Wenn die Aktion „delete“ lautet, führe die Methode [delete] aus,
  • Zeile 12: Rufe den Termin für den ausgewählten Terminblock ab. Zur Erinnerung: [selectedSlot] wurde mit dem Verweis auf das ausgewählte [DoctorSlot]-Element initialisiert;
  • Wenn dieser Termin existiert, wird er gelöscht (Zeile 19), der Kalender wird aktualisiert (Zeile 21) und anschließend neu angezeigt (Zeile 23),
  • wenn das Löschen fehlgeschlagen ist, wird die Fehlerseite angezeigt (Zeile 26),
  • Wenn das ausgewählte Element keinen Termin enthält (Zeile 13), befinden wir uns in einer Situation, in der der Benutzer auf [Löschen] in einem Zeitfenster geklickt hat, das keinen Termin enthält. Wir melden diesen Fehler:
 

Die Methode [reportIncorrectAction] lautet wie folgt:


// 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);
  }
  • Zeile 4: Auswahl entfernen,
  • Zeile 6: Erzeuge eine internationalisierte Fehlermeldung,
  • Zeile 7: Füge das Attribut („error“, true) zum Args-Wörterbuch des AJAX-Aufrufs hinzu.

Kehren wir zum XHTML-Code für die Schaltfläche [Ja] zurück:


<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
oncomplete="handleRequest(xhr, status, args); confirmation.hide()">
  • Zeile 2: Nach der Ausführung der Methode [Form].action wird die JavaScript-Methode handleRequest ausgeführt:

    <!-- 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> 
  • Zeile 10: Wir prüfen, ob das Wörterbuch „args“ ein Attribut namens „error“ enthält. Ist dies der Fall, wird das Dialogfeld aus Zeile 2 angezeigt.
  • Zeile 3: Es wird die von der Vorlage generierte Fehlermeldung angezeigt.

6.11. Einen Termin vereinbaren

Die Terminvereinbarung erfolgt in folgender Reihenfolge:

Die bei dieser Aktion verwendete Ansicht sieht wie folgt aus:


<!-- 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>
...
  • Zeilen 21–31: Folgendes anzeigen:
  • Zeile 21: Dies wird angezeigt, wenn keine Termine vorhanden sind, eine Auswahl getroffen wurde und die ID des ausgewählten Zeitfensters mit der der aktuellen Tabellenzeile übereinstimmt. Wenn diese Bedingung nicht enthalten ist, wird das Fragment für alle Zeitfenster angezeigt,
  • Zeile 22: Das Eingabefeld wird ein assistiertes Eingabefeld sein. Wir gehen hier davon aus, dass es viele Kunden geben kann,
  • Zeilen 24–26: der Link [Absenden],
  • Zeilen 28–30: der Link [Abbrechen].

Das Autovervollständigungsfeld wird durch den folgenden Code generiert:


<p:autoComplete completeMethod="#{form.completeClients}" value="#{form.identiteClient}" size="30"/>

Die Methode [Form].completeClients ist dafür zuständig, dem Benutzer Vorschläge auf der Grundlage der in das Eingabefeld eingegebenen Zeichen zu unterbreiten:

 

Die Vorschläge haben das Format [Nachname, Vorname, Titel]. Der Code für die Methode [Form].completeClients lautet wie folgt:


  // 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();
}
  • Zeile 2: query ist die vom Benutzer eingegebene Zeichenkette,
  • Zeile 3: die Liste der Vorschläge. Zu Beginn eine leere Liste,
  • Zeilen 5–10: Wir bilden die vollständigen Namen der Kunden [Nachname, Vorname, Titel]. Wenn ein vollständiger Name mit query beginnt (Zeile 7), wird er zur Liste der Vorschläge hinzugefügt (Zeile 8).

6.12. Bestätigung eines Termins

Die Bestätigung eines Termins erfolgt in folgender Reihenfolge:

Der Code für den Link [Validieren] lautet wie folgt:


          <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>

Die Methode [Form].action() wird dieses Ereignis also verarbeiten. In der Zwischenzeit hat das Modell [Form].action die Zeichenfolge „submit“ erhalten. Der Code lautet wie folgt:


  // 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;
}
  • Zeilen 33–35: Aufgrund des Werts des Felds „action“ wird die Methode [validateReservation] ausgeführt.
  • Zeile 43: Wir prüfen zunächst, ob der Kunde existiert. Der Grund dafür ist, dass der Benutzer im Feld für die unterstützte Eingabe möglicherweise Daten manuell eingegeben hat, ohne die angebotenen Vorschläge zu nutzen. Die unterstützte Eingabe ist mit dem Modell [Form].identiteClient verknüpft. Wir prüfen daher, ob diese Identität im Wörterbuch identitesClients vorhanden ist, das bei der Instanziierung des Modells erstellt wurde (Zeile 20). Dieses Wörterbuch verknüpft eine Kundenidentität vom Typ [Nachname, Vorname, Titel] mit dem Kunden selbst (Zeile 25),
  • Zeile 44: Wenn der Kunde nicht existiert, wird ein Fehler an den Browser zurückgegeben,
  • Zeile 45: eine internationalisierte Fehlermeldung,
  • Zeile 46: Wir fügen das Attribut („error“, true) zum Wörterbuch „args“ des AJAX-Aufrufs hinzu. Der AJAX-Aufruf wurde wie folgt definiert:

<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>

In Zeile 3 oben sehen wir, dass der Link [Validate] ein oncomplete-Attribut hat. Dieses Attribut sorgt dafür, dass die Fehlermeldung mithilfe einer Technik angezeigt wird, die wir bereits kennengelernt haben.

  • Zeile 50: Wir weisen die [business]-Schicht an, einen Termin für den ausgewählten Tag (day), das ausgewählte Zeitfenster (creneauChoisi.getCreneau()) und den ausgewählten Kunden (hIdentitesClients.get(identiteClient)) hinzuzufügen,
  • Zeile 52: Wir weisen die [Business]-Schicht an, den Kalender des Arztes zu aktualisieren. Wir sehen den hinzugefügten Termin sowie alle Änderungen, die andere Nutzer der Anwendung möglicherweise vorgenommen haben,
  • Zeile 54: Der Kalender [form2.xhtml] wird erneut angezeigt,
  • Zeile 57: Bei einem Fehler wird die Fehlerseite angezeigt.

6.13. Einen Termin absagen

Dies entspricht der folgenden Abfolge:

Die Schaltfläche [Abbrechen] auf der Seite [form2.xhtml] sieht wie folgt aus:


<p:commandLink action="#{form.action()}" value="#{msg['form2.annuler']}" update=":formulaire:contenu">
            <f:setPropertyActionListener value="annuler" target="#{form.action}"/>
          </p:commandLink>

Die Methode [Form].action wird daher aufgerufen:


// 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. Navigieren im Kalender

Über die Symbolleiste können Sie im Kalender navigieren:

In den obigen Screenshots nicht zu sehen: Der Kalender wird mit den Terminen für den neu ausgewählten Tag aktualisiert.

Die Tags für die drei entsprechenden Schaltflächen lauten in [form1.xhtml] wie folgt:


  <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>

Die Methoden [Form].getPreviousAgenda, [Form].getNextAgenda und [Form].today lauten wie folgt:


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();
    }
}
  • Zeile 1: der Tag, an dem der Kalender angezeigt wird,
  • Zeile 5: Wir verwenden einen Kalender,
  • Zeile 6: der auf den aktuellen Tag des Kalenders initialisiert ist,
  • Zeile 7: Wir ziehen einen Tag vom Kalender ab,
  • Zeile 8: und setzen ihn auf das Anzeigedatum des Kalenders zurück,
  • Zeile 11: Wir zeichnen den Kalender neu, wenn er gerade angezeigt wird. Der Grund dafür ist, dass der Benutzer die Symbolleiste auch dann nutzen kann, wenn der Kalender nicht angezeigt wird.

Die anderen Methoden funktionieren ähnlich.

6.15. Ändern der Anzeigesprache

Die Sprachumschaltung erfolgt über die Menüschaltfläche in der Symbolleiste:

Die Menü-Schaltflächen sind wie folgt beschriftet:


<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>

Die in der Vorlage ausgeführten Methoden lauten wie folgt:


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);
    }
}

Die Methoden in den Zeilen 3 und 9 initialisieren lediglich das Feld local aus Zeile 1 und leiten den Browser des Clients anschließend auf dieselbe Seite weiter. Eine Weiterleitung ist eine Antwort, bei der der Server den Browser anweist, eine andere Seite zu laden. Der Browser führt daraufhin eine GET-Anfrage an diese neue Seite aus.

  • Zeile 17: [ExternalContext] ist eine JSF-Klasse, die Zugriff auf das aktuell ausgeführte Servlet bietet,
  • Zeile 19: Wir führen die Weiterleitung durch. Der Parameter der redirect-Methode ist die URL der Seite, zu der der Client-Browser weitergeleitet werden soll. Hier möchten wir zu [/mv-rdvmedecins-pf] weiterleiten, was der Name unserer Anwendung ist:
  

Die Methode [getRequestContextPath] liefert diesen Namen. Daher wird die Startseite [index.xhtml] unserer Anwendung geladen. Diese Seite ist mit dem [Form]-Modell im Sitzungsbereich verknüpft. Dieses Modell verwaltet drei boolesche Werte, die das Erscheinungsbild der Seite [index.xhtml] steuern:


  private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;

Da das Modell im Session-Bereich liegt, haben diese drei Booleschen Werte ihre Werte beibehalten. Die Seite [index.xhtml] wird daher so angezeigt, wie sie vor der Weiterleitung war. Diese Seite wird mithilfe der folgenden Facelet-Vorlage [layout.xhtml] formatiert:


<?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>

Das Tag in Zeile 9 legt die Anzeigesprache der Seite mithilfe des Attributs locale fest. Die Seite wechselt daher je nach Bedarf zu Französisch oder Englisch. Aber warum eine Weiterleitung? Kehren wir zu den Tags für die Sprachwechseloptionen zurück:


<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>

Ursprünglich wurden sie geschrieben, um den Formular-ID-Bereich über einen AJAX-Aufruf zu aktualisieren (das „update“-Attribut in den Zeilen 7 und 8). Während der Tests funktionierte der Sprachwechsel jedoch nicht immer. Daher die Weiterleitung, um dieses Problem zu beheben. Wir hätten auch das Attribut „ajax='false'“ in den Tags setzen können, um einen Seitenneuladung auszulösen. Das hätte die Weiterleitung vermieden.

6.16. Aktualisieren der Listen

Dies entspricht der folgenden Aktion:

 

Das mit der Option [Aktualisieren] verbundene Tag lautet wie folgt:


<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>  

Die Methode [Form].refresh lautet wie folgt:


  public void refresh() {
    // on rafraîchit les listes
    init();
}

Die Methode „init“ wird unmittelbar nach der Initialisierung der [Form]-Bean ausgeführt. Ihr Zweck besteht darin, Daten aus der Datenbank im Modell zwischenzuspeichern:


// 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);
    }
  }

Die Methode init*<span style="color: #000000"> erstellt die Listen und Wörterbücher in den Zeilen 5–9. Der Nachteil dieser Technik besteht darin, dass diese Elemente Änderungen in der Datenbank (Hinzufügen eines Kunden, eines Arztes usw.) nicht mehr widerspiegeln. Die Methode refresh* erzwingt die Neugenerierung dieser Listen und Wörterbücher. Daher werden wir sie jedes Mal aufrufen, wenn eine Änderung an der Datenbank vorgenommen wird, beispielsweise beim Hinzufügen eines neuen Kunden.

6.17. Fazit

Lassen Sie uns die Architektur der soeben erstellten Anwendung noch einmal betrachten:

Wir haben uns stark auf die vorgefertigte JSF2-Version gestützt:

  • Die [Business]-, [DAO]- und [JPA]-Schichten wurden beibehalten,
  • die [Application]- und [Form]-Beans in der Webschicht wurden beibehalten, jedoch wurden ihnen aufgrund der Verbesserung der Benutzeroberfläche neue Funktionen hinzugefügt,
  • die Benutzeroberfläche wurde erheblich überarbeitet. Insbesondere ist sie nun funktionsreicher und benutzerfreundlicher.

Der Übergang von JSF zu PrimeFaces für die Erstellung der Weboberfläche erfordert etwas Erfahrung, da man zunächst von der großen Anzahl verfügbarer Komponenten etwas überwältigt ist und letztlich nicht ganz sicher ist, welche man verwenden soll. Man muss sich daher auf die gewünschte Benutzerfreundlichkeit der Oberfläche konzentrieren.

6.18. Eclipse-Tests

Wie bereits bei früheren Versionen der Beispielanwendung zeigen wir, wie man diese Version 03 mit Eclipse testet. Zunächst importieren wir die Maven-Projekte für Beispiel 03 [1] in Eclipse:

  • [mv-rdvmedecins-ejb-dao-jpa]: die [DAO]- und [JPA]-Schichten,
  • [mv-rdvmedecins-ejb-metier]: die [Business]-Schicht,
  • [mv-rdvmedecins-pf]: die mit JSF und PrimeFaces implementierte [Web]-Schicht,
  • [mv-rdvmedecins-app]: das übergeordnete Projekt des Unternehmensprojekts [mv-rdvmedecins-app-ear]. Beim Import des übergeordneten Projekts wird das untergeordnete Projekt automatisch mitimportiert,
  • Führen Sie in [2] das Unternehmensprojekt [mv-rdvmedecins-app-ear] aus,
  • Wählen Sie in [3] den Glassfish-Server aus,
  • in [4], auf der Registerkarte [Servers], wurde die Anwendung bereitgestellt. Sie wird nicht automatisch ausgeführt. Sie müssen die URL [http://localhost:8080/mv-rdvmedecins-pf/] in einem Browser aufrufen [5]: