6. تطبيق example-03: rdvmedecins-pf-ejb
لنتذكر بنية التطبيق النموذجي الذي تم تطويره لخادم Glassfish:
![]() |
لن نغير أي شيء في هذه البنية باستثناء طبقة الويب التي سيتم تنفيذها هنا باستخدام JSF و Primefaces.
![]() |
6.1. مشروع Netbeans
في الأعلى، الطبقتان [métier] و [DAO] هما نفس الطبقتين الموجودتين في المثال 01 JSF / EJB / Glassfish. سنعيد استخدامهما.
![]() |
- [mv-rdvmedecins-ejb-dao-jpa]: مشروع EJB للطبقات [DAO] و [JPA] من المثال 01،
- [mv-rdvmedecins-ejb-metier]: مشروع EJB للطبقة [métier] من المثال 01،
- [mv-rdvmedecins-pf]: مشروع الطبقة [web] / Primefaces – جديد،
- [mv-rdvmedecins-app-ear]: مشروع مؤسسي لنشر التطبيق على خادم Glassfish – جديد.
6.2. مشروع المؤسسة
يستخدم مشروع المؤسسة فقط لنشر الوحدات الثلاث [mv-rdvmedecins-ejb-dao-jpa] و [mv-rdvmedecins-ejb-metier] و [mv-rdvmedecins-pf] على خادم Glassfish. مشروع Netbeans هو التالي:
![]() |
لا يوجد المشروع إلا لهذه التبعيات الثلاث [1] المحددة في الملف [pom.xml] على النحو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>mv-rdvmedecins-app</artifactId>
<groupId>istia.st</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>istia.st</groupId>
<artifactId>mv-rdvmedecins-app-ear</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>ear</packaging>
<name>mv-rdvmedecins-app-ear</name>
...
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>${project.version}</version>
<type>ejb</type>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-metier</artifactId>
<version>${project.version}</version>
<type>ejb</type>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-pf</artifactId>
<version>${project.version}</version>
<type>war</type>
</dependency>
</dependencies>
</project>
- الأسطر 10-13: أداة Maven الخاصة بمشروع المؤسسة،
- الأسطر 18-37: التبعيات الثلاثة للمشروع. لاحظ نوع هذه التبعيات (الأسطر 23 و29 و35).
لتشغيل تطبيق الويب، سيتعين تشغيل مشروع المؤسسة هذا.
6.3. مشروع الويب Primefaces
مشروع الويب Primefaces هو التالي:
![]() |
- في [1]، صفحات المشروع. الصفحة [index.xhtml] هي الصفحة الوحيدة في المشروع. وهي تتضمن ثلاثة أجزاء هي [form1.xhtml] و [form2.xhtml] و [erreur.xhtml]. أما الصفحات الأخرى فهي موجودة فقط من أجل التنسيق.
- في [2]، حبات Java. حبة [Application] ذات نطاق application، وحبة [Form] ذات نطاق session. تغلف الفئة [Erreur] خطأً. تُستخدم الفئة [MyDataModel] كنموذج لعلامة <dataTable> في Primefaces،
- في [3]، ملفات الرسائل للتدويل،
- في [4]، التبعيات. يعتمد مشروع الويب على مشروع EJB من الطبقة [DAO]، ومشروع EJB من الطبقة [métier] و Primefaces للطبقة [web].
6.4. تكوين المشروع
تكوين المشروع هو نفس تكوين مشاريع Primefaces أو JSF التي درسناها. ندرج ملفات التكوين دون إعادة شرحها.
![]() | ![]() |
[web.xml]: تكوين تطبيق الويب.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Production</param-value>
</context-param>
<context-param>
<param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
<error-page>
<error-code>500</error-code>
<location>/faces/exception.xhtml</location>
</error-page>
<error-page>
<exception-type>Exception</exception-type>
<location>/faces/exception.xhtml</location>
</error-page>
</web-app>
يُلاحظ في السطر 30 أن الصفحة [index.xhtml] هي الصفحة الرئيسية للتطبيق.
[faces-config.xml]: تكوين التطبيق JSF
<?xml version='1.0' encoding='UTF-8'?>
<!-- =========== FULL CONFIGURATION FILE ================================== -->
<faces-config version="2.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">
<application>
<resource-bundle>
<base-name>
messages
</base-name>
<var>msg</var>
</resource-bundle>
<message-bundle>messages</message-bundle>
</application>
</faces-config>
[beans.xml]: فارغة ولكنها ضرورية للتعليق التوضيحي @Named
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>
[styles.css]: ورقة أنماط التطبيق
.col1{
background-color: #ccccff
}
.col2{
background-color: #ffcccc
}
تأتي مكتبة Primefaces مع أوراق الأنماط الخاصة بها. لا تُستخدم ورقة الأنماط المذكورة أعلاه إلا للصفحة التي يتم عرضها في حالة حدوث استثناء، وهي صفحة لا تديرها التطبيق. وعندئذ يتم عرض الصفحة [exception.xhtml].
[messages_fr.properties]: ملف الرسائل باللغة الفرنسية
# التخطيط
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers - application propuls\u00e9e par PrimeFaces et JQuery
# استثناء
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
# نموذج 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
# نموذج 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.titre=Une erreur s'est produite.
erreur.exceptions=Cha\u00eene des exceptions
erreur.type=Type de l'exception
erreur.message=Message associ\u00e9
erreur.accueil=Accueil
[messages_en.properties]: ملف الرسائل باللغة الإنجليزية
# التخطيط
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university - Application powered by PrimeFaces and JQuery
# استثناء
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
# نموذج 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
# نموذج 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.titre=The following exceptions occurred
erreur.exceptions=Exceptions' chain
erreur.type=Exception type
erreur.message=Associated Message
erreur.accueil=Welcome
6.5. قالب الصفحات [layout.xhtml]
![]() |
القالب [layout.xhtml] هو التالي:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{form.locale}">
<h:head>
<title>JSF</title>
<h:outputStylesheet library="css" name="styles.css"/>
</h:head>
<h:body style="background-image: url('#{request.contextPath}/resources/images/standard.jpg');">
<h:form id="formulaire">
<table style="width: 1200px">
<tr>
<td colspan="2" bgcolor="#ccccff">
<ui:include src="entete.xhtml"/>
</td>
</tr>
<tr>
<td style="width: 10px;" bgcolor="#ffcccc">
<ui:include src="menu.xhtml"/>
</td>
<td>
<p:outputPanel id="contenu">
<ui:insert name="contenu">
<h2>Contenu</h2>
</ui:insert>
</p:outputPanel>
</td>
</tr>
<tr bgcolor="#ffcc66">
<td colspan="2">
<ui:include src="basdepage.xhtml"/>
</td>
</tr>
</table>
</h:form>
</h:body>
</f:view>
</html>
الجزء المتغير الوحيد في هذا النموذج هو المنطقة الموجودة في الأسطر 28-30. توجد هذه المنطقة في منطقة id:formulaire:contenu (السطر 27). سنحتفظ بهذا في ذاكرتنا. ستحمل استدعاءات AJAX التي تقوم بتحديث هذه المنطقة السمة update=":formulaire:contenu". من ناحية أخرى، يبدأ النموذج في السطر 15. وبالتالي، يتم إدراج الجزء المضاف في الأسطر 28-30 في هذا النموذج.
المظهر الذي يقدمه هذا النموذج هو التالي:
![]() |
سيتم إدراج الجزء الديناميكي من الصفحة في المنطقة المحددة أعلاه.
6.6. الصفحة [index.xhtml]
![]() |
يعرض المشروع دائمًا نفس الصفحة، وهي الصفحة التالية [index.xhtml]:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="layout.xhtml">
<ui:define name="contenu">
<ui:fragment rendered="#{form.form1Rendered}">
<ui:include src="form1.xhtml"/>
</ui:fragment>
<ui:fragment rendered="#{form.form2Rendered}">
<ui:include src="form2.xhtml"/>
</ui:fragment>
<ui:fragment rendered="#{form.erreurRendered}">
<ui:include src="erreur.xhtml"/>
</ui:fragment>
</ui:define>
</ui:composition>
</html>
- السطران 8-9: سيتم إدراج هذا الجزء XHTML في المنطقة الديناميكية للنموذج [layout.xhtml]،
- تتضمن الصفحة ثلاثة أجزاء فرعية:
- [form1.xhtml]، الأسطر 10-12؛
- [form2.xhtml]، الأسطر 13-15؛
- [erreur.xhtml]، الأسطر 16-18.
يتم التحكم في وجود هذه الأجزاء في [index.xhtml] بواسطة قيم منطقية من النموذج [Form.java] المرتبط بالصفحة. وبالتالي، عند التلاعب بهذه القيم، تختلف الصفحة المعروضة.
يتم عرض الجزء [form1.xhtml] على النحو التالي:
يتم عرض الجزء [form2.xhtml] على النحو التالي:
![]() |
يتم عرض الجزء [erreur.xhtml] على النحو التالي:
![]() |
6.7. حبوب المشروع
![]() |
تمت مناقشة فئة الحزمة [utils] سابقًا: فئة [Messages] هي فئة تسهل تدويل رسائل التطبيق. تمت دراستها في الفقرة 2.8.5.7.
6.7.1. البيان Application
البيان [Application.java] هو بيان من نطاق application. نتذكر أن هذا النوع من البيان يستخدم لتخزين البيانات للقراءة فقط والمتاحة لجميع مستخدمي التطبيق. هذا البيان هو التالي:
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: نسمي البين "application"،
- السطر 9: نطاقه هو application،
- السطران 13-14: سيتم حقن مرجع على الواجهة المحلية للطبقة [métier] بواسطة الحاوية EJB الخاصة بخادم التطبيق. لنتذكر بنية التطبيق:
![]() |
سيتم تنفيذ التطبيق JSF و EJB و [Metier] في نفس JVM (Java Virtual Machine). لذلك ستستخدم الطبقة [JSF] الواجهة المحلية لـ EJB. هذا كل شيء. لا يحتوي bean [Application] على أي شيء آخر. للوصول إلى الطبقة [métier]، ستبحث البينات الأخرى عنها في هذا البين.
6.7.2. البيان [Erreur]
الفئة [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: رسالة خطأ.
6.7.3. البيان [Form]
ورمزه هو التالي:
package beans;
import java.io.IOException;
...
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// فول التطبيق
@Inject
private Application application;
// ذاكرة التخزين المؤقتة للجلسة
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>();
// النموذج
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() {
// نضع الأطباء والعملاء في ذاكرة التخزين المؤقت
try {
medecins = application.getMetier().getAllMedecins();
clients = application.getMetier().getAllClients();
} catch (Throwable th) {
// تسجيل الخطأ
prepareVueErreur(th);
return;
}
// القواميس
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
hIdentitesClients.put(identite(c), c);
}
}
...
// عرض المنظر
private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean erreurRendered) {
this.form1Rendered = form1Rendered;
this.form2Rendered = form2Rendered;
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(true, false, true);
}
// مُستردات ومُعيّنات
...
}
- السطور 6-8: الفئة [Form] هي bean باسم form ونطاق session. نذكر أن الفئة يجب أن تكون قابلة للتسلسل،
- السطور 14-15: يحتوي bean form على مرجع إلى bean application. سيتم حقن هذا المرجع بواسطة حاوية servlets التي يتم فيها تشغيل التطبيق (وجود التعليق التوضيحي @Inject).
- الأسطر 17-44: قالب الصفحات [form1.xhtml, form2.xhtml, erreur.xhtml]. يتم التحكم في عرض هذه الصفحات من خلال القيم المنطقية الموجودة في الأسطر 27-29. تجدر الإشارة إلى أنه بشكل افتراضي، يتم عرض الصفحة [form1.xhtml] (السطر 27)،
- السطور 46-47: يتم تنفيذ الطريقة init مباشرة بعد إنشاء مثيل للفئة (وجود التعليق التوضيحي @PostConstruct)،
- السطور 50-51: يُطلب من الطبقة [métier] قائمة الأطباء والعملاء،
- الأسطر 59-65: إذا سارت الأمور على ما يرام، يتم إنشاء قواميس الأطباء والعملاء. يتم فهرستها حسب أرقامها. بعد ذلك، سيتم عرض الصفحة [form1.xhtml] (السطر 27)،
- السطر 54: في حالة حدوث خطأ، يتم إنشاء نموذج الصفحة [erreur.xhtml]. هذا النموذج هو قائمة الأخطاء في السطر 36،
- الأسطر 78-88: تقوم الطريقة [prepareVueErreur] بإنشاء قائمة الأخطاء المراد عرضها. ثم تعرض الصفحة [index.xhtml] الأجزاء [form1.xhtml] و [erreur.xhtml] (السطر 87).
الصفحة [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:ui="http://java.sun.com/jsf/facelets">
<body>
<p:panel header="#{msg['erreur.titre']}" closable="true" >
<hr/>
<p:dataTable value="#{form.erreurs}" var="erreur">
<f:facet name="header">
<h:outputText value="#{msg['erreur.exceptions']}"/>
</f:facet>
<p:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.type']}"/>
</f:facet>
<h:outputText value="#{erreur.classe}"/>
</p:column>
<p:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.message']}"/>
</f:facet>
<h:outputText value="#{erreur.message}"/>
</p:column>
</p:dataTable>
</p:panel>
</body>
</html>
وهي تستخدم علامة <p:dataTable> (الأسطر 12-28) لعرض قائمة الأخطاء. وهذا يعطي صفحة خطأ مشابهة لما يلي:
![]() |
سنقوم الآن بتحديد المراحل المختلفة لحياة التطبيق. لكل إجراء يقوم به المستخدم، سندرس العروض ذات الصلة ومديري الأحداث.
6.8. عرض الصفحة الرئيسية
إذا سارت الأمور على ما يرام، فإن الصفحة الأولى التي يتم عرضها هي [form1.xhtml]. وهذا يعطي العرض التالي:
![]() |
الصفحة [form1.xhtml] هي التالية:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<p:toolbar>
<p:toolbarGroup align="left">
...
</p:toolbarGroup>
<p:toolbarGroup align="right">
...
</p:toolbarGroup>
</p:toolbar>
</html>
شريط الأدوات المحدد في لقطة الشاشة هو مكون Primefaces Toolbar. يتم تعريفه في الأسطر 8-14. ويحتوي على مجموعتين من المكونات، يتم تعريف كل منهما بواسطة علامة <toolbarGroup>، في الأسطر 9-11 و12-14. أحد المجموعتين محاذي لليسار من شريط الأدوات (السطر 9)، والآخر محاذي لليمين (السطر 12).
دعونا نلقي نظرة على بعض مكونات المجموعة اليسرى:
<p:toolbar>
<p:toolbarGroup align="left">
<h:outputText value="#{msg['form1.medecin']}"/>
<p:selectOneMenu value="#{form.idMedecin}" effect="fade">
<f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>
<p:ajax update=":formulaire:contenu" listener="#{form.hideAgenda}" />
</p:selectOneMenu>
<p:separator/>
<h:outputText value="#{msg['form1.jour']}"/>
<p:calendar id="calendrier" value="#{form.jour}" readOnlyInputText="true">
<p:ajax event="dateSelect" listener="#{form.hideAgenda}" update=":formulaire:contenu"/>
</p:calendar>
<p:separator/>
<p:commandButton id="resa-agenda" icon="ui-icon-check" actionListener="#{form.getAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-agenda" value="#{msg['form1.agenda']}"/>
...
</p:toolbarGroup>
...
- الأسطر 4-7: قائمة الأطباء التي تمت إضافة تأثير إليها (effect="fade")،
- السطر 6: سلوك AJAX. عند حدوث تغيير في القائمة المنسدلة، سيتم تنفيذ الأسلوب [Form].hideAgenda (listener="#{form.hideAgenda}") وسيتم تحديث المنطقة الديناميكية :form:content (update=":formulaire:contenu")،
- السطر 8: يتضمن فاصلًا في شريط الأدوات،
- الأسطر 10-12: منطقة إدخال التاريخ. نستخدم هنا تقويم Primefaces. منطقة الإدخال هي للقراءة فقط (readOnlyInputText="true")،
- السطر 11: سلوك AJAX. عند حدوث تغيير في التاريخ، سيتم تنفيذ الطريقة [Form].hideAgenda وسيتم تحديث المنطقة الديناميكية :form:content،
- السطر 14: زر. يؤدي النقر عليه إلى تنفيذ استدعاء AJAX إلى الطريقة [Form].getAgenda ()، وسيتم عندئذ تعديل النموذج واستخدام استجابة الخادم لتحديث المنطقة الديناميكية :formulaire:contenu،
- السطر 15: تسمح علامة <tooltip> بربط فقاعة مساعدة بمكون. يتم تحديد معرف هذا الأخير بواسطة السمة for في tooltip. هنا (for="resa-agenda") تشير إلى الزر في السطر 14:
![]() | ![]() |
يتم تشغيل هذه الصفحة بواسطة النموذج التالي:
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// ذاكرة التخزين المؤقتة للجلسة
private List<Medecin> medecins;
private List<Client> clients;
// القالب
private Long idMedecin;
private Date jour = new Date();
// قائمة الأطباء
public List<Medecin> getMedecins() {
return medecins;
}
// قائمة العملاء
public List<Client> getClients() {
return clients;
}
// المفكرة
public void getAgenda() {
...
}
- يقوم الحقل في السطر 12 بتغذية قيمة القائمة في السطر 4 من الصفحة للقراءة والكتابة. عند العرض الأولي للصفحة، تحدد القيمة المحددة في القائمة المنسدلة. عند العرض الأولي، idMedecin يساوي null، وبالتالي سيتم تحديد الطبيب الأول،
- تقوم الطريقة في الأسطر 16-18 بإنشاء عناصر قائمة الأطباء المنسدلة (السطر 5 من الصفحة). سيكون لكل خيار تم إنشاؤه تسمية (itemLabel) تتضمن اللقب واسم العائلة والاسم الأول للطبيب، وقيمة (itemValue) تمثل معرف الطبيب،
- ويقوم حقل السطر 13 بتغذية حقل الإدخال في السطر 10 من الصفحة بالقراءة/الكتابة. عند العرض الأولي، يتم عرض تاريخ اليوم،
- الأسطر 26-28: تعالج الطريقة getAgenda النقر على الزر [Agenda] الموجود في السطر 14 من الصفحة. وهي مطابقة تقريبًا لما كانت عليه في الإصدار JSF:
// فئة التطبيق
@Inject
private Application application;
// ذاكرة التخزين المؤقتة للجلسة
private List<Medecin> medecins;
private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
// القالب
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 {
// استرداد الطبيب
medecin = hMedecins.get(idMedecin);
// جدول أعمال الطبيب ليوم معين
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// عرض النموذج 2
setForms(true, true, false);
} catch (Throwable th) {
// عرض الأخطاء
prepareVueErreur(th);
}
// لم يتم اختيار أي موعد حتى الآن
creneauChoisi = null;
}
لن نعلق على هذا الرمز. فقد تم ذلك بالفعل.
6.9. عرض جدول مواعيد الطبيب
6.9.1. نظرة عامة على جدول المواعيد
هذا هو حالة الاستخدام التالية:
![]() |
- في [1]، يتم اختيار طبيب [1] ويوم [2] ثم يتم طلب [3] جدول مواعيد الطبيب لليوم المختار،
- في [4]، يظهر هذا الجدول أسفل شريط الأدوات.
رمز الصفحة [form2.xhtml] هو التالي:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jsp/jstl/core">
<body>
<!-- القائمة السياقية -->
<p:contextMenu for="agenda">
...
</p:contextMenu>
<!-- جدول المواعيد -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- عمود المواعيد -->
<p:column style="width: 100px">
...
</p:column>
<!-- عمود العملاء -->
<p:column style="width: 300px">
...
</p:column>
</p:dataTable>
<!-- تأكيد الحذف RV -->
<p:confirmDialog id="confirmDialog" message="#{msg['form2.suppression.confirmation']}"
header="#{msg['form2.suppression.message']}" severity="alert" widgetVar="confirmation">
...
</p:confirmDialog>
<!-- رسالة خطأ -->
<p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >
...
</p:dialog>
<!-- إدارة استجابة الخادم -->
<script type="text/javascript">
...
}
</script>
</body>
</html>
- الأسطر 16-26: العنصر الرئيسي للصفحة هو الجدول <dataTable> الذي يعرض جدول مواعيد الطبيب،
![]() |
- الأسطر 12-14: سنستخدم قائمة سياقية لإضافة/حذف موعد:
![]() |
- الأسطر 29-32: سيتم عرض مربع تأكيد عندما يرغب المستخدم في حذف موعد:
![]() |
- الأسطر 35-37: سيتم استخدام مربع حوار للإبلاغ عن خطأ:
![]() |
- الأسطر 40-43: سنحتاج إلى إدخال القليل من جافا سكريبت.
6.9.2. جدول المواعيد
نتناول هنا نموذج جدول البيانات كما تمت دراسته في الفقرة 5.15، الصفحة 327.
دعونا نلقي نظرة على العنصر الرئيسي في الصفحة، وهو الجدول الذي يعرض الأجندة:
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- عمود المواعيد -->
<p:column style="width: 100px">
...
</p:column>
<!-- عمود العملاء -->
<p:column style="width: 300px">
...
</p:column>
</p:dataTable>
والنتيجة هي كما يلي:
![]() |
إنه جدول من عمودين (السطور 4-6 و8-10) يتم تغذيته من المصدر [Form].getMyDataModel() (value="#{form.myDataModel}"). يمكن تحديد سطر واحد فقط في كل مرة (selectionMode="single"). في كل مرة يتم فيها POST، يتم تعيين مرجع للعنصر المحدد إلى [Form].creneauChoisi (selection="#{form.creneauChoisi}").
نتذكر أن الطريقة getAgenda قامت بتهيئة الحقل التالي في النموذج:
// القالب
private AgendaMedecinJour agendaMedecinJour;
يتم الحصول على نموذج الجدول عن طريق استدعاء الطريقة [Form].getMyDataModel (السمة value للعلامة <dataTable>) التالية:
// نموذج dataTable
public MyDataModel getMyDataModel() {
return new MyDataModel(agendaMedecinJour.getCreneauxMedecinJour());
}
دعونا نلقي نظرة على الفئة [MyDataModel] التي تُستخدم كنموذج لعلامة <p:dataTable>:
package beans;
import javax.faces.model.ArrayDataModel;
import org.primefaces.model.SelectableDataModel;
import rdvmedecins.metier.entites.CreneauMedecinJour;
public class MyDataModel extends ArrayDataModel<CreneauMedecinJour> implements SelectableDataModel<CreneauMedecinJour> {
// الشركات المصنعة
public MyDataModel() {
}
public MyDataModel(CreneauMedecinJour[] creneauxMedecinJour) {
super(creneauxMedecinJour);
}
@Override
public Object getRowKey(CreneauMedecinJour creneauMedecinJour) {
return creneauMedecinJour.getCreneau().getId();
}
@Override
public CreneauMedecinJour getRowData(String rowKey) {
// قائمة الفترات الزمنية
CreneauMedecinJour[] creneauxMedecinJour = (CreneauMedecinJour[]) getWrappedData();
// المفتاح هو عدد صحيح طويل
long key = Long.parseLong(rowKey);
// البحث عن الفترة الزمنية المحددة
for (CreneauMedecinJour creneauMedecinJour : creneauxMedecinJour) {
if (creneauMedecinJour.getCreneau().getId().longValue() == key) {
return creneauMedecinJour;
}
}
// لا شيء
return null;
}
}
- السطر 7: الفئة [MyDataModel] هي نموذج العلامة <p:dataTable>. تهدف هذه الفئة إلى الربط بين عنصر rowkey الذي يتم إرساله، والعنصر المرتبط بهذا السطر،
- السطر 7: تنفذ الفئة واجهة [SelectableDataModel] من خلال الفئة [ArrayDataModel]. وهذا يعني أن معلمة المنشئ هي مصفوفة. وهذه المصفوفة هي التي تغذي العلامة <dataTable>. وهنا سيتم ربط كل سطر من المصفوفة بعنصر من النوع [CreneauMedecinJour]،
- السطور 13-15: يقوم المنشئ بتمرير معلمته إلى فئته الأصلية،
- الأسطر 18-20: كل سطر في المصفوفة يتوافق مع فترة زمنية وسيتم تعريفه بواسطة معرف الفترة الزمنية (السطر 19). هذا المعرف هو الذي سيتم إرساله إلى الخادم،
- السطر 23: الكود الذي سيتم تنفيذه على جانب الخادم عند إرسال معرف فترة زمنية. الهدف من هذه الطريقة هو إرجاع مرجع الكائن [CreneauMedecinJour] المرتبط بهذا المعرف. سيتم تعيين هذا المرجع إلى هدف السمة selection لعلامة <dataTable>:
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
وبالتالي، سيحتوي الحقل [Form].creneauChoisi على مرجع الكائن [CreneauMedecinJour] الذي نريد إضافته أو حذفه.
6.9.3. عمود الفترات الزمنية
![]() |
يتم الحصول على عمود الفترات الزمنية باستخدام الكود التالي:
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- عمود المواعيد -->
<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>
<!-- عمود العملاء -->
<p:column style="width: 300px">
...
</p:column>
</p:dataTable>
- السطور 5-7: عنوان العمود،
- السطور 8-15: العنصر الحالي للعمود. تجدر الإشارة في السطر 9 إلى استخدام العلامة <h:outputFormat> التي تسمح بتنسيق العناصر المراد عرضها. تشير المعلمة value إلى سلسلة الأحرف المراد عرضها. تشير الصيغة {i,type,format} إلى المعلمة رقم i ونوع هذه المعلمة وتنسيقها. يوجد هنا 4 معلمات مرقمة من 0 إلى 3، ونوعها رقمي وسيتم عرضها برقمين،
- السطور 10-13: المعلمات الأربعة التي تتوقعها العلامة <h:outputFormat>.
6.9.4. عمود العملاء
![]() |
يتم الحصول على عمود العملاء باستخدام الكود التالي:
<!-- التقويم -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- عمود المواعيد -->
...
<!-- عمود العملاء -->
<p:column style="width: 300px">
<f:facet name="header">
<h:outputText value="#{msg['form2.client']}"/>
</f:facet>
<ui:fragment rendered="#{creneauMedecinJour.rv!=null}">
<h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}" />
</ui:fragment>
<ui:fragment rendered="#{creneauMedecinJour.rv==null and form.creneauChoisi!=null and form.creneauChoisi.creneau.id==creneauMedecinJour.creneau.id}">
...
</ui:fragment>
</p:column>
</p:dataTable>
- الأسطر 8-10: عنوان العمود،
- السطور 11-13: العنصر الحالي عندما يكون هناك موعد في الفترة الزمنية. في هذه الحالة، يتم عرض اللقب والاسم الأول واسم العائلة للعميل الذي تم تحديد هذا الموعد له،
- السطور 14-16: جزء آخر سنعود إليه لاحقًا.
6.10. حذف موعد
يتم حذف موعد وفقًا للتسلسل التالي:
![]() |
![]() |
الطريقة المعنية بهذا الإجراء هي التالية:
<!-- القائمة السياقية -->
<p:contextMenu for="agenda">
...
<p:menuitem value="#{msg['form2.supprimer']}" onclick="confirmation.show()"/>
</p:contextMenu>
<!-- التقويم -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
...
</p:dataTable>
<!-- تأكيد الحذف RV -->
<p:confirmDialog id="confirmDialog" message="#{msg['form2.suppression.confirmation']}"
header="#{msg['form2.suppression.message']}" severity="alert" widgetVar="confirmation">
<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
oncomplete="handleRequest(xhr, status, args); confirmation.hide()">
<f:setPropertyActionListener value="supprimer" target="#{form.action}"/>
</p:commandButton>
<p:commandButton value="#{msg['form2.supprimer.non']}" onclick="confirmation.hide()" type="button" />
</p:confirmDialog>
- السطور 2-5: قائمة سياقية مرتبطة بجدول البيانات (السمة for). وتحتوي على خيارين [1]:
![]() |
- السطر 4: الخيار [Supprimer] يؤدي إلى عرض مربع الحوار [2] للسطور 13-20،
- السطر 15: يؤدي النقر على [Oui] إلى تشغيل [Form.action] الذي سيحذف الموعد. عادةً، لا ينبغي أن تعرض القائمة السياقية الخيار [Supprimer] إذا كان العنصر المحدد لا يحتوي على موعد، ولا الخيار [Réserver] إذا كان العنصر المحدد يحتوي على موعد. لم ننجح في جعل القائمة السياقية بهذه الدقة. ننجح في ذلك بالنسبة للعنصر الأول المحدد، ولكن بعد ذلك نلاحظ أن القائمة السياقية تحتفظ بالإعداد الذي تم تحديده لهذا الاختيار الأول. وبالتالي تصبح غير صحيحة. لذلك احتفظنا بالخيارين وقررنا تزويد المستخدم بتعليق إذا قام بحذف عنصر لا يحتوي على موعد،
- السطر 16: السمة oncomplete التي تسمح بتحديد كود جافا سكريبت ليتم تنفيذه بعد تنفيذ استدعاء AJAX. سيكون هذا الكود كما يلي:
<!-- رسالة خطأ -->
<p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >
<h:outputText value="#{form.msgErreur}" />
</p:dialog>
<!-- إدارة استجابة الخادم -->
<script type="text/javascript">
function handleRequest(xhr, status, args) {
// خطأ؟
if(args.erreur) {
dlgErreur.show();
}
}
</script>
- السطر 10: يتحقق كود جافا سكريبت مما إذا كان القاموس args يحتوي على السمة erreur. إذا كان الأمر كذلك، فإنه يعرض مربع الحوار الموجود في السطر 2 (السمة widgetVar). يعرض هذا المربع النموذج [Form].msgErreur.
لنلقِ نظرة على الكود الذي يتم تنفيذه لإدارة حذف موعد:
<p:confirmDialog ...>
<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
...>
<f:setPropertyActionListener value="supprimer" target="#{form.action}"/>
</p:commandButton>
...
</p:confirmDialog>
- السطر 2: سيتم تنفيذ الطريقة [Form].action،
- السطر 4: قبل تنفيذها، سيكون الحقل action قد تلقى القيمة 'supprimer'.
الطريقة [action] هي كما يلي:
// إجراء على RV
public void action() {
// حسب الإجراء المطلوب
if (action.equals("supprimer")) {
supprimer();
}
...
}
public void supprimer() {
// هل يجب القيام بشيء ما؟
Rv rv = creneauChoisi.getRv();
if (rv == null) {
signalerActionIncorrecte();
return;
}
try {
// حذف موعد
application.getMetier().supprimerRv(rv);
// تحديث الأجندة
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// عرض النموذج 2
setForms(true, true, false);
} catch (Throwable th) {
// عرض الأخطاء
prepareVueErreur(th);
}
// مسح الفترة الزمنية المختارة
creneauChoisi = null;
}
- السطر 4: إذا كانت الإجراء هو "حذف"، يتم تنفيذ الطريقة [supprimer]،
- السطر 12: يتم استرداد الموعد الخاص بالفترة الزمنية المحددة. تجدر الإشارة إلى أن [creneauChoisi] تم تهيئته بواسطة مرجع العنصر [CreneauMedecinJour] المحدد،
- إذا كان هذا الموعد موجودًا، يتم حذفه (السطر 19)، ويتم إعادة إنشاء التقويم (السطر 21) ثم إعادة عرضه (السطر 23)،
- إذا فشل الحذف، يتم عرض صفحة الأخطاء (السطر 26)،
- إذا لم يكن للعنصر المحدد موعد (السطر 13)، فإننا نكون في حالة قام فيها المستخدم بالنقر على [Supprimer] في فترة زمنية لا تحتوي على موعد. يتم الإبلاغ عن هذا الخطأ:
![]() |
الطريقة [signalerActionIncorrecte] هي كما يلي:
// الإبلاغ عن إجراء غير صحيح
private void signalerActionIncorrecte() {
// إلغاء الفترة الزمنية المختارة
creneauChoisi = null;
// خطأ
msgErreur = Messages.getMessage(null, "form2.erreurAction", null).getSummary();
RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
}
- السطر 4: يتم إلغاء التحديد،
- السطر 6: يتم إنشاء رسالة خطأ دولية،
- السطر 7: نضيف إلى قاموس args الخاص بالاستدعاء AJAX السمة ('erreur', true).
لنعد إلى الكود XHTML الخاص بالزر [Oui]:
<p:commandButton value="#{msg['form2.supprimer.oui']}" update=":formulaire:contenu" action="#{form.action}"
oncomplete="handleRequest(xhr, status, args); confirmation.hide()">
- السطر 2: بعد تنفيذ الطريقة [Form].action، يتم تنفيذ طريقة Javascript handleRequest:
<!-- رسالة خطأ -->
<p:dialog header="#{msg['form2.erreur']}" widgetVar="dlgErreur" height="100" >
<h:outputText value="#{form.msgErreur}" />
</p:dialog>
<!-- إدارة استجابة الخادم -->
<script type="text/javascript">
function handleRequest(xhr, status, args) {
// خطأ؟
if(args.erreur) {
dlgErreur.show();
}
}
</script>
- السطر 10: يتم التحقق مما إذا كان القاموس args يحتوي على السمة المسماة 'erreur'. إذا كان الأمر كذلك، يتم عرض مربع الحوار في السطر 2،
- السطر 3: يعرض رسالة الخطأ التي تم إنشاؤها بواسطة النموذج.
6.11. تحديد موعد
يتم حجز الموعد وفقًا للتسلسل التالي:
![]() |
الطريقة المعنية بهذا الإجراء هي التالية:
<!-- القائمة السياقية -->
<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>
<!-- التقويم -->
<p:dataTable id="agenda" value="#{form.myDataModel}" var="creneauMedecinJour" style="width: 800px"
selectionMode="single" selection="#{form.creneauChoisi}" emptyMessage="#{msg['form2.emtyMessage']}">
<!-- عمود المواعيد -->
<p:column style="width: 100px">
...
</p:column>
<!-- عمود العملاء -->
<p:column style="width: 300px">
<f:facet name="header">
<h:outputText value="#{msg['form2.client']}"/>
</f:facet>
...
<ui:fragment rendered="#{creneauMedecinJour.rv==null and form.creneauChoisi!=null and form.creneauChoisi.creneau.id==creneauMedecinJour.creneau.id}">
<p:autoComplete completeMethod="#{form.completeClients}" value="#{form.identiteClient}" size="30"/>
<p:spacer width="50px"/>
<p:commandLink action="#{form.action()}" value="#{msg['form2.valider']}" update=":formulaire:contenu" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="valider" target="#{form.action}"/>
</p:commandLink>
<p:spacer width="50px"/>
<p:commandLink action="#{form.action()}" value="#{msg['form2.annuler']}" update=":formulaire:contenu">
<f:setPropertyActionListener value="annuler" target="#{form.action}"/>
</p:commandLink>
</ui:fragment>
</p:column>
</p:dataTable>
...
- السطور 21-31: تعرض ما يلي:
![]() |
- السطر 21: يتم العرض إذا لم يكن هناك موعد، وكان هناك اختيار بالفعل، وكان معرف الفترة الزمنية المختارة مطابقًا لمعرف العنصر الحالي في الجدول. إذا لم يتم وضع هذا الشرط، فسيتم عرض المقتطف لجميع الفترات الزمنية،
- السطر 22: ستكون منطقة الإدخال منطقة إدخال بمساعدة. نفترض هنا أنه قد يكون هناك العديد من العملاء،
- الأسطر 24-26: الرابط [Valider]،
- الأسطر 28-30: الرابط [Annuler].
يتم إنشاء منطقة الإدخال المساعدة بواسطة الرمز التالي:
<p:autoComplete completeMethod="#{form.completeClients}" value="#{form.identiteClient}" size="30"/>
تتولى الطريقة [Form].completeClients تقديم اقتراحات للمستخدم بناءً على الأحرف المكتوبة في منطقة الإدخال:
![]() |
تكون الاقتراحات بالشكل [Nom prénom titre]. رمز الطريقة [Form].completeClients هو التالي:
// طريقة الإكمال التلقائي للنص
public List<String> completeClients(String query) {
List<String> identites = new ArrayList<String>();
// البحث عن العملاء المطابقين
for (Client c : clients) {
String identite = identite(c);
if (identite.toLowerCase().startsWith(query.toLowerCase())) {
identites.add(identite);
}
}
return identites;
}
private String identite(Client c) {
return c.getNom() + " " + c.getPrenom() + " " + c.getTitre();
}
- السطر 2: query هي سلسلة الأحرف التي كتبها المستخدم،
- السطر 3: قائمة الاقتراحات. في البداية تكون القائمة فارغة،
- الأسطر 5-10: يتم إنشاء هويات [Nom prénom titre] للعملاء. إذا كانت الهوية تبدأ بـ query (السطر 7)، يتم الاحتفاظ بها في قائمة الاقتراحات (السطر 8).
6.12. التحقق من صحة الموعد
يتم التحقق من صحة الموعد وفقًا للتسلسل التالي:
![]() |
رمز الرابط [Valider] هو التالي:
<p:commandLink action="#{form.action()}" value="#{msg['form2.valider']}" update=":formulaire:contenu" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="valider" target="#{form.action}"/>
</p:commandLink>
وبالتالي، فإن الطريقة [Form].action() هي التي ستدير هذا الحدث. في غضون ذلك، سيكون النموذج [Form].action قد تلقى السلسلة 'valider'. الرمز هو كما يلي:
// فئة التطبيق
@Inject
private Application application;
// ذاكرة التخزين المؤقتة للجلسة
...
private Map<String, Client> hIdentitesClients = new HashMap<String, Client>();
// القالب
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);
}
}
// إجراء على RV
public void action() {
// حسب الإجراء المطلوب
...
if (action.equals("valider")) {
validerResa();
}
}
// تأكيد الموعد
public void validerResa() {
// التحقق من الحجز
try {
// هل العميل موجود؟
Boolean erreur = !hIdentitesClients.containsKey(identiteClient);
if (erreur) {
msgErreur = Messages.getMessage(null, "form2.erreurClient", new Object[]{identiteClient}).getSummary();
RequestContext.getCurrentInstance().addCallbackParam("erreur", true);
return;
}
// إضافة الموعد
application.getMetier().ajouterRv(jour, creneauChoisi.getCreneau(), hIdentitesClients.get(identiteClient));
// تحديث الأجندة
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// عرض النموذج 2
setForms(true, true, false);
} catch (Throwable th) {
// عرض الأخطاء
prepareVueErreur(th);
}
// إلغاء الفترة الزمنية المختارة
creneauChoisi = null;
// إلغاء العميل
identiteClient = null;
}
- الأسطر 33-35: بسبب قيمة الحقل action، سيتم تنفيذ الطريقة [validerResa]،
- السطر 43: يتم أولاً التحقق من وجود العميل. في الواقع، في منطقة الإدخال المساعد، تمكن المستخدم من إجراء إدخال ثابت دون الاستعانة بالاقتراحات التي قُدمت له. يرتبط الإدخال المساعد بالنموذج [Form].identiteClient. لذلك، نبحث عما إذا كانت هذه الهوية موجودة في القاموس identitesClients الذي تم إنشاؤه عند إنشاء مثيل النموذج (السطر 20). يربط هذا القاموس هوية عميل من النوع [Nom prénom titre] بالعميل نفسه (السطر 25)،
- السطر 44: إذا كان العميل غير موجود، يتم إرسال خطأ إلى المتصفح،
- السطر 45: رسالة خطأ مدوللة،
- السطر 46: نضيف السمة ('erreur',true) إلى قاموس args الخاص بالاستدعاء AJAX. تم تعريف الاستدعاء AJAX على النحو التالي:
<p:commandLink action="#{form.action()}" value="#{msg['form2.valider']}" update=":formulaire:contenu" oncomplete="handleRequest(xhr, status, args)">
<f:setPropertyActionListener value="valider" target="#{form.action}"/>
</p:commandLink>
في السطر 3 أعلاه، نرى أن الرابط [Valider] يحتوي على سمة oncomplete. هذه السمة هي التي ستعرض رسالة الخطأ وفقًا لتقنية سبق أن تناولناها.
- السطر 50: يُطلب من الطبقة [métier] إضافة موعد لليوم المحدد (jour)، والفترة الزمنية المختارة (creneauChoisi.getCreneau()) والعميل المختار (hIdentitesClients.get(identiteClient))،
- السطر 52: يُطلب من الطبقة [métier] تحديث جدول مواعيد الطبيب. سنرى الموعد المضاف بالإضافة إلى جميع التعديلات التي قد يكون قام بها مستخدمون آخرون للتطبيق،
- السطر 54: يتم إعادة عرض جدول المواعيد [form2.xhtml]،
- السطر 57: يتم عرض صفحة الخطأ في حالة حدوث خطأ.
6.13. إلغاء موعد
وهذا يتوافق مع التسلسل التالي:
![]() |
الزر [Annuler] في الصفحة [form2.xhtml] هو التالي:
<p:commandLink action="#{form.action()}" value="#{msg['form2.annuler']}" update=":formulaire:contenu">
<f:setPropertyActionListener value="annuler" target="#{form.action}"/>
</p:commandLink>
وبالتالي يتم استدعاء الطريقة [Form].action:
// إجراء على RV
public void action() {
// حسب الإجراء المطلوب
...
if (action.equals("annuler")) {
annulerRv();
}
}
// إلغاء حجز الموعد
public void annulerRv() {
// يتم عرض النموذج 2
setForms(true, true, false);
// مسح الفترة الزمنية المختارة
creneauChoisi = null;
// مسح بيانات العميل
identiteClient = null;
}
6.14. التنقل في التقويم
تتيح شريط الأدوات التنقل في التقويم:
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
لا يظهر في لقطات الشاشة أعلاه، يتم تحديث التقويم بمواعيد اليوم الجديد المختار.
علامات العناصر الثلاثة المعنية هي التالية في [form1.xhtml]:
<p:toolbar>
<p:toolbarGroup align="left">
...
<h:outputText value="#{msg['form1.jour']}"/>
<p:calendar id="calendrier" value="#{form.jour}" readOnlyInputText="true">
<p:ajax event="dateSelect" listener="#{form.hideAgenda}" update=":formulaire:contenu"/>
</p:calendar>
<p:separator/>
<p:commandButton id="resa-agenda" icon="ui-icon-check" actionListener="#{form.getAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-agenda" value="#{msg['form1.agenda']}"/>
<p:commandButton id="resa-precedent" icon="ui-icon-seek-prev" actionListener="#{form.getPreviousAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-precedent" value="#{msg['form1.precedent']}"/>
<p:commandButton id="resa-suivant" icon="ui-icon-seek-next" actionListener="#{form.getNextAgenda}" update=":formulaire:contenu"/>
<p:tooltip for="resa-suivant" value="#{msg['form1.suivant']}"/>
<p:commandButton id="resa-today" icon="ui-icon-home" actionListener="#{form.today}" update=":formulaire:contenu"/>
<p:tooltip for="resa-today" value="#{msg['form1.today']}"/>
</p:toolbarGroup>
<p:toolbarGroup align="right">
...
</p:toolbarGroup>
</p:toolbar>
الطرق التالية هي [Form].getPreviousAgenda، [Form].getNextAgenda، [Form].today:
private Date jour = new Date();
public void getPreviousAgenda() {
// الانتقال إلى اليوم السابق
Calendar cal = Calendar.getInstance();
cal.setTime(jour);
cal.add(Calendar.DAY_OF_YEAR, -1);
jour = cal.getTime();
// التقويم
if (form2Rendered) {
getAgenda();
}
}
public void getNextAgenda() {
// الانتقال إلى اليوم التالي
Calendar cal = Calendar.getInstance();
cal.setTime(jour);
cal.add(Calendar.DAY_OF_YEAR, 1);
jour = cal.getTime();
// التقويم
if (form2Rendered) {
getAgenda();
}
}
// جدول اليوم
public void today() {
jour = new Date();
// جدول الأعمال
if (form2Rendered) {
getAgenda();
}
}
- السطر 1: يوم عرض التقويم،
- السطر 5: يتم استخدام تقويم،
- السطر 6: يتم تهيئته ليكون اليوم الحالي في التقويم،
- السطر 7: نخصم يومًا من التقويم،
- السطر 8: ثم نعيد التعيين ليكون يوم عرض التقويم،
- السطر 11: نعيد عرض التقويم إذا كان معروضًا. في الواقع، يمكن للمستخدم استخدام شريط الأدوات دون أن يكون التقويم معروضًا.
الطرق الأخرى مشابهة.
6.15. تغيير لغة العرض
يتم إدارة تغيير اللغة من خلال زر القائمة في شريط الأدوات:
![]() |
![]() |
علامات زر القائمة هي كما يلي:
<p:toolbar>
<p:toolbarGroup align="left">
...
</p:toolbarGroup>
<p:toolbarGroup align="right">
<p:menuButton value="#{msg['form1.options']}">
<p:menuitem id="menuitem-francais" value="#{msg['form1.francais']}" actionListener="#{form.setFrenchLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-anglais" value="#{msg['form1.anglais']}" actionListener="#{form.setEnglishLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>
</p:menuButton>
</p:toolbarGroup>
</p:toolbar>
الطرق التي يتم تنفيذها في النموذج هي التالية:
private String locale = "fr";
public void setFrenchLocale() {
locale = "fr";
// إعادة تحميل الصفحة
redirect();
}
public void setEnglishLocale() {
locale = "en";
// إعادة تحميل الصفحة
redirect();
}
private void redirect() {
// إعادة توجيه العميل إلى السيرفلت
ExternalContext ctx = FacesContext.getCurrentInstance().getExternalContext();
try {
ctx.redirect(ctx.getRequestContextPath());
} catch (IOException ex) {
Logger.getLogger(Form.class.getName()).log(Level.SEVERE, null, ex);
}
}
تقتصر الطرق في السطرين 3 و9 على تهيئة الحقل locale في السطر 1 ثم إعادة توجيه متصفح العميل إلى نفس الصفحة. إعادة التوجيه هي استجابة يطلب فيها الخادم من المتصفح تحميل صفحة أخرى. ثم يقوم المتصفح بإجراء GET إلى هذه الصفحة الجديدة.
- السطر 17: [ExternalContext] هي فئة JSF تسمح بالوصول إلى السيرفلت قيد التشغيل،
- السطر 19: يتم إجراء إعادة التوجيه. معلمة الطريقة redirect هي URL للصفحة التي يجب أن يعيد توجيه المتصفح العميل إليها. هنا نريد إعادة التوجيه إلى [/mv-rdvmedecins-pf] وهو اسم تطبيقنا:
![]() |
تسمح الطريقة [getRequestContextPath] بالحصول على هذا الاسم. وبالتالي، سيتم تحميل الصفحة الرئيسية [index.xhtml] لتطبيقنا. ترتبط هذه الصفحة بالنموذج [Form] ذي نطاق الجلسة. يدير هذا النموذج ثلاث قيم منطقية تتحكم في مظهر الصفحة [index.xhtml]:
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean erreurRendered = false;
نظرًا لأن النموذج ذو نطاق جلسة، فقد احتفظت هذه القيم الثلاثة بقيمها. وبالتالي، ستظهر الصفحة [index.xhtml] في الحالة التي كانت عليها قبل إعادة التوجيه. يتم تنسيق هذه الصفحة باستخدام قالب facelet [layout.xhtml] التالي:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{form.locale}">
....
</f:view>
</html>
تحدد العلامة في السطر 9 لغة عرض الصفحة باستخدام سمة locale. وبالتالي، ستظهر الصفحة باللغة الفرنسية أو الإنجليزية حسب الحالة. والآن، لماذا يتم إعادة التوجيه؟ لنعد إلى علامات خيارات تغيير اللغة:
<p:toolbar>
<p:toolbarGroup align="left">
...
</p:toolbarGroup>
<p:toolbarGroup align="right">
<p:menuButton value="#{msg['form1.options']}">
<p:menuitem id="menuitem-francais" value="#{msg['form1.francais']}" actionListener="#{form.setFrenchLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-anglais" value="#{msg['form1.anglais']}" actionListener="#{form.setEnglishLocale}" update=":formulaire"/>
<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>
</p:menuButton>
</p:toolbarGroup>
</p:toolbar>
كانت قد كُتبت في البداية لتحديث منطقة معرف النموذج (السمة update في السطرين 7 و8) عن طريق استدعاء AJAX. ولكن عند الاختبار، لم يعمل تغيير اللغة في كل مرة. ومن هنا جاءت إعادة التوجيه لحل هذه المشكلة. ربما كان من الممكن أيضًا وضع السمة ajax='false' في العلامات لإحداث إعادة تحميل للصفحة. كان ذلك سيتجنب إعادة التوجيه.
6.16. تحديث القوائم
وهذا يتوافق مع الإجراء التالي:
![]() |
العلامة المرتبطة بالخيار [Rafraîchir] هي التالية:
<p:menuitem id="menuitem-rafraichir" value="#{msg['form1.rafraichir']}" actionListener="#{form.refresh}" update=":formulaire:contenu"/>
الطريقة [Form].refresh هي التالية:
public void refresh() {
// يتم تحديث القوائم
init();
}
الطريقة init هي الطريقة التي يتم تنفيذها مباشرة بعد إنشاء bean [Form]. والغرض منها هو تخزين بيانات قاعدة البيانات مؤقتًا في النموذج:
// فول التطبيق
@Inject
private Application application;
// ذاكرة التخزين المؤقتة للجلسة
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() {
// يتم تخزين الأطباء والعملاء في ذاكرة التخزين المؤقت
try {
medecins = application.getMetier().getAllMedecins();
clients = application.getMetier().getAllClients();
} catch (Throwable th) {
...
}
...
// القواميس
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
hIdentitesClients.put(identite(c), c);
}
}
تقوم الطريقة init بإنشاء القوائم والقواميس الواردة في الأسطر 5-9. عيب هذه التقنية هو أن هذه العناصر لا تأخذ في الاعتبار التغييرات التي تطرأ على قاعدة البيانات (إضافة عميل، أو طبيب، ...). تفرض الطريقة refresh إعادة إنشاء هذه القوائم والقواميس. لذلك، سيتم استخدامها في كل مرة يتم فيها إجراء تغيير في قاعدة البيانات، مثل إضافة عميل جديد على سبيل المثال.
6.17. الخلاصة
دعونا نستعرض بنية التطبيق الذي قمنا بإنشائه للتو:
![]() |
اعتمدنا بشكل كبير على الإصدار JSF2 الذي تم إنشاؤه بالفعل:
- تم الاحتفاظ بالطبقات [métier] و [DAO] و [JPA]،
- تم الاحتفاظ بـ beans [Application] و [Form] من طبقة الويب ولكن تمت إضافة وظائف جديدة إليها بسبب إثراء واجهة المستخدم،
- تم تعديل واجهة المستخدم بشكل جذري. وهي الآن أكثر ثراءً من حيث الميزات وأكثر سهولة في الاستخدام.
يتطلب الانتقال من JSF إلى Primefaces لبناء واجهة الويب خبرة معينة، لأننا في البداية نشعر بالارتباك قليلاً أمام العدد الكبير من المكونات القابلة للاستخدام، وفي النهاية لا نعرف بالضبط أيها يجب استخدامه. لذا، يجب التركيز على سهولة الاستخدام المطلوبة للواجهة.
6.18. اختبارات Eclipse
كما فعلنا مع الإصدارات السابقة من التطبيق النموذجي، نعرض كيفية اختبار هذا الإصدار 03 باستخدام Eclipse. أولاً، نقوم باستيراد مشاريع Maven الخاصة بالمثال 03 [1] إلى Eclipse:
![]() |
- [mv-rdvmedecins-ejb-dao-jpa]: الطبقتان [DAO] و [JPA]،
- [mv-rdvmedecins-ejb-metier]: الطبقة [métier]،
- [mv-rdvmedecins-pf]: الطبقة [web] التي تم تنفيذها باستخدام JSF و Primefaces،
- [mv-rdvmedecins-app]: الأصل لمشروع المؤسسة [mv-rdvmedecins-app-ear]. عند استيراد المشروع الأصلي، يتم استيراد المشروع الفرعي تلقائيًا،
- في [2]، يتم تنفيذ مشروع المؤسسة [mv-rdvmedecins-app-ear]،
![]() |
- في [3]، يتم اختيار خادم Glassfish،
- في [4]، في علامة التبويب [Servers]، تم نشر التطبيق. لا يتم تشغيله تلقائيًا. يجب طلب تشغيله في متصفح:
![]() |
















































