7. Ajaxificazione di un’applicazione Spring MVC
7.1. Il ruolo di AJAX in un’applicazione web
Per il momento, gli esempi di apprendimento esaminati presentavano la seguente architettura:
![]() |
Per passare da una vista [Vue1] a una vista [Vue2], il browser:
- invia una richiesta all’applicazione web;
- riceve la vista [Vue2] e la visualizza al posto della vista [Vue1].
Questo è lo schema classico:
- richiesta del browser;
- generazione di una vista in risposta al client da parte del server web;
- visualizzazione di questa nuova vista da parte del browser.
Da alcuni anni esiste un altro modo di interazione tra il browser e il server web: AJAX (Asynchronous Javascript And Xml). Si tratta infatti di interazioni tra la vista visualizzata dal browser e il server web. Il browser continua a fare ciò che sa fare, ovvero visualizzare una vista HTML, ma ora viene gestito da JavaScript incorporato nella vista HTML visualizzata. Lo schema è il seguente:
![]() |
- in [1], si verifica un evento nella pagina visualizzata nel browser (clic su un pulsante, modifica di un testo, ...). Questo evento viene intercettato dal JavaScript (jS) incorporato nella pagina;
- in [2], il codice JavaScript effettua una richiesta HTTP proprio come avrebbe fatto il browser. La richiesta è asincrona: l’utente può continuare a interagire con la pagina senza essere bloccato dall’attesa della risposta alla richiesta HTTP. La richiesta segue il classico processo di elaborazione. Nulla (o quasi) la distingue da una richiesta tradizionale;
- in [3], viene inviata una risposta al client jS. Piuttosto che una vista completa HTML, viene inviata una vista parziale HTML, un flusso XML o jSON (notazione JavaScript) che viene inviato;
- in [4], il JavaScript recupera questa risposta e la utilizza per aggiornare una sezione della pagina HTML visualizzata.
Per l’utente si verifica un cambiamento nella visualizzazione poiché ciò che vede è cambiato. Tuttavia, non avviene un ricaricamento totale della pagina, ma semplicemente una modifica parziale della pagina visualizzata. Ciò contribuisce a conferire fluidità e interattività alla pagina: poiché non vi è un ricaricamento totale della pagina, è possibile gestire eventi che in precedenza non venivano gestiti. Ad esempio, proporre all’utente un elenco di opzioni man mano che digita i caratteri in una casella di immissione. Ad ogni nuovo carattere digitato, viene inviata una richiesta AJAX al server, che a sua volta restituisce ulteriori suggerimenti. Senza Ajax, questo tipo di assistenza alla digitazione era in precedenza impossibile. Non era possibile ricaricare una nuova pagina ad ogni carattere digitato.
7.2. Aggiornamento di una pagina con un flusso HTML
7.2.1. Le viste
Ci proponiamo di esaminare la seguente applicazione:
![]() |
- in [1], l’ora di caricamento della pagina;
- in [2], si eseguono le quattro operazioni aritmetiche su due numeri reali A e B;
- in [3], la risposta del server viene inserita in una sezione della pagina;
- in [4], l'ora del calcolo. Questa è diversa dall'ora di caricamento della pagina [5]. Quest'ultimo è pari a [1], il che dimostra che l'area [6] non è stata ricaricata. Inoltre, i valori URL e [7] della pagina non sono cambiati.
7.2.2. L'azione [/ajax-01]
![]() |
Il controller [Ajax.java] definisce la seguente azione [/ajax-01]:
@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
// tempo valido?
if (tempo != null) {
boolean valide = false;
int valueTempo = 0;
try {
valueTempo = Integer.parseInt(tempo);
valide = valueTempo >= 0;
} catch (NumberFormatException e) {
}
if (valide) {
session.setAttribute("tempo", new Integer(valueTempo));
}
}
// si prepara il modello della vista [vue-01]
...
}
- riga 2: l'azione [/ajax-01] accetta un solo parametro, [tempo]. Si tratta del tempo, espresso in millisecondi, che il server dovrà attendere prima di inviare i risultati delle operazioni aritmetiche;
- riga 4: il parametro [tempo] è facoltativo;
- righe 5-12: si verifica che il valore del parametro [tempo] sia accettabile;
- righe 13-15: se lo è, il valore del tempo di attesa viene memorizzato nella sessione. Ciò significa che rimarrà in vigore finché non verrà modificato;
Il codice dell'azione [/ajax-01] prosegue come segue:
@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
// intervallo di tempo valido?
...
// si sta preparando il modello della vista [vue-01]
modèle.addAttribute("actionModel01", new ActionModel01());
...
// vista
return "vue-01";
}
La classe [ActionModel01] serve principalmente a incapsulare i valori inviati dall'azione [/ajax-01]. In questo caso, non viene inviato nulla. Si crea una classe vuota che viene inserita nel modello poiché la vista [vue-01.xml] la utilizza. La classe [ActionModel01] è la seguente:
package istia.st.springmvc.models;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
public class ActionModel01 {
// dati inviati
@NotNull
@DecimalMin(value = "0.0")
private Double a;
@NotNull
@DecimalMin(value = "0.0")
private Double b;
// getter e setter
...
}
- righe 11 e 15: due valori reali [a,b] che verranno inviati tramite un modulo;
Torniamo al codice dell'azione:
@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
...
// si prepara il modello della vista [vue-01]
modèle.addAttribute("actionModel01", new ActionModel01());
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
...
// vista
return "vue-01";
}
- righe 6-7: si inserisce un'istanza di tipo [Resultats] nel modello;
Il tipo [Resultats] inserito nel modello è il seguente:
![]() |
package istia.st.springmvc.models;
public class Resultats {
// dati
private String aplusb;
private String amoinsb;
private String amultiplieparb;
private String adiviseparb;
private String heureGet;
private String heurePost;
private String erreur;
private String vue;
private String culture;
// getter e setter
...
}
- righe 6-9: il risultato delle quattro operazioni aritmetiche sui numeri [a,b];
- riga 10: l'ora del caricamento iniziale della pagina;
- riga 11: l'ora di esecuzione delle quattro operazioni aritmetiche;
- riga 12: un eventuale messaggio di errore;
- riga 13: l'eventuale vista da visualizzare;
- riga 14: la cultura della vista, [fr-FR] o [en-US];
Il codice dell'azione [/ajax-01] prosegue come segue:
@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
...
// impostazioni locali
setLocale(locale, modèle, résultats);
...
}
- riga 5: il metodo [setLocale] serve a inserire nel modello della vista la cultura da utilizzare, [fr-FR] o [en-US]. Questa cultura è destinata al JavaScript incorporato nella vista;
Il metodo [setLocale] è il seguente:
private void setLocale(Locale locale, Model modèle, Resultats résultats) {
// si gestiscono solo le impostazioni locali fr-FR e en-US
String language = locale.getLanguage();
String country = null;
switch (language) {
case "fr":
country = "FR";
break;
default:
language = "en";
country = "US";
break;
}
// cultura
résultats.setCulture(String.format("%s-%s", language, country));
}
Nel modello sarà presente la stringa [${resultats.culture}] pari a 'fr-FR' o 'en-US'.
Torniamo all’azione [/ajax-01]:
@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
...
// impostazioni locali
setLocale(locale, modèle, résultats);
// ora
résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
// vista
return "vue-01";
}
- riga 7: si inserisce l’ora di GET nel modello;
- riga 9: si visualizza la vista [vue-01.xml]:
7.2.3. La vista [vue-01.xml]
![]() | ![]() |
La vista [vue-01.xml] è la seguente:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-01</title>
<link rel="stylesheet" href="/css/ajax01.css" />
<script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/jquery/jquery.validate.min.js"></script>
<script type="text/javascript" src="/js/jquery/jquery.validate.unobtrusive.min.js"></script>
<script type="text/javascript" src="/js/jquery/globalize/globalize.js"></script>
<script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.fr-FR.js"></script>
<script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.en-US.js"></script>
<script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>
<script type="text/javascript" src="/js/json3.js"></script>
<script type="text/javascript" src="/js/client-validation.js"></script>
<script type="text/javascript" src="/js/local1.js"></script>
<script th:inline="javascript">
/*<![CDATA[*/
var culture = [[${resultats.culture}]];
Globalize.culture(culture);
/*]]>*/
</script>
</head>
<body>
<h2>Ajax - 01</h2>
<p>
<strong th:text="#{labelHeureGetCulture(${resultats.heureGet},${resultats.culture})}">
Heure de chargement :
</strong>
</p>
<h4>
<p th:text="#{titre.part1}">
Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls
</p>
</h4>
<form id="formulaire" name="formulaire" ... ">
...
</form>
<hr />
<div id="resultats" />
</body>
</html>
- righe 7-12: le librerie jQuery di convalida e internazionalizzazione (culture);
- riga 15: la libreria [client-validation] creata al paragrafo 6.3;
- riga 14: la libreria jSON utilizzata dalla libreria [client-validation]. È facoltativa se i log di convalida sono stati disattivati;
- riga 13: la libreria [Unobtrusive Ajax] di Microsoft. Questa libreria a volte consente di evitare di scrivere codice JavaScript;
- riga 16: un file jS per le nostre esigenze specifiche;
- righe 17-22: per gestire lato client le impostazioni di formato [fr-FR] e [en-US]. Abbiamo già incontrato questo codice;
- riga 27: un messaggio personalizzato. Li abbiamo esaminati nel paragrafo 5.18;
- righe 36-38: il modulo su cui torneremo;
- riga 40: l’area del documento in cui il JavaScript inserirà la risposta del server;
7.2.4. Il modulo
![]() |
Nella vista [vue-01.xml], il modulo è il seguente:
<form id="formulaire" name="formulaire" th:action="@{/ajax-02.html}" method="post" th:object="${actionModel01}" th:attr="data-ajax='true',data-ajax-loading='#loading',data-ajax-loading-duration='0',data-ajax-method='post',data-ajax-mode='replace',data-ajax-update='#resultats', data-ajax-begin='beforeSend',data-ajax-complete='afterComplete' ">
<table>
<thead>
<tr>
<th>
<span th:text="#{valeur.a}"></span>
</th>
<th>
<span th:text="#{valeur.b}"></span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input type="text" th:field="*{a}" th:value="*{a}" data-val="true"
th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.a.min},data-val-min-value=#{actionModel01.a.min.value}" />
</td>
<td>
<input type="text" th:field="*{b}" th:value="*{b}" data-val="true"
th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.b.min},data-val-min-value=#{actionModel01.b.min.value}" />
</td>
</tr>
<tr>
<td>
<span class="field-validation-valid" data-valmsg-for="a" data-valmsg-replace="true"></span>
<span th:if="${#fields.hasErrors('a')}" th:errors="*{a}" class="error">Donnée
erronée
</span>
</td>
<td>
<span class="field-validation-valid" data-valmsg-for="b" data-valmsg-replace="true"></span>
<span th:if="${#fields.hasErrors('b')}" th:errors="*{b}" class="error">Donnée
erronée
</span>
</td>
</tr>
</tbody>
</table>
<p>
<input type="submit" th:value="#{action.calculer}" value="Calculer"></input>
<img id="loading" style="display: none" src="/images/loading.gif" />
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
</p>
</form>
che genera il seguente HTML:
<form id="formulaire" name="formulaire" method="post" data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading" action="/ajax-02.html">
<table>
<thead>
<tr>
<th>
<span>valeur de A</span>
</th>
<th>
<span>valeur de B</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input type="text" data-val="true" data-val-min="Le nombre doit être supérieur ou égal à 0" data-val-number="Format invalide" data-val-min-value="0" data-val-required="Le champ est obligatoire" value="" id="a" name="a" />
</td>
<td>
<input type="text" data-val="true" data-val-min="Le nombre doit être supérieur ou égal à 0" data-val-number="Format invalide" data-val-min-value="0" data-val-required="Le champ est obligatoire" value="" id="b" name="b" />
</td>
</tr>
<tr>
<td>
<span class="field-validation-valid" data-valmsg-for="a" data-valmsg-replace="true"></span>
</td>
<td>
<span class="field-validation-valid" data-valmsg-for="b" data-valmsg-replace="true"></span>
</td>
</tr>
</tbody>
</table>
<p>
<input type="submit" value="Calculer" />
<img id="loading" style="display: none" src="/images/loading.gif" />
<a href="javascript:postForm()">Calculer</a>
</p>
</form>
- riga 16: al campo [a] sono associati i validatori [required], [number] e [min];
- riga 19: lo stesso vale per il campo [b];
I vari messaggi si trovano nei file [messages.properties] del progetto:
![]() |
[messages_fr.properties]
NotNull=Le champ est obligatoire
typeMismatch=Format invalide
actionModel01.a.min=Le nombre doit être supérieur ou égal à 0
DecimalMin.actionModel01.a=Le nombre doit être supérieur ou égal à 0
DecimalMax.actionModel01.b=Le nombre doit être supérieur ou égal à 0
actionModel01.b.min=Le nombre doit être supérieur ou égal à 0
valeur.a=valeur de A
valeur.b=valeur de B
actionModel01.a.min.value=0
actionModel01.b.min.value=0
labelHeureCalcul=Heure de calcul :
LabelErreur=Une erreur s''est produite : [{0}]
labelAplusB=A+B=
labelAmoinsB=A-B=
labelAfoisB=A*B=
labelAdivB=A/B=
titre.part1=Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls
labelHeureGetCulture=Heure de chargement : [{0}], culture : [{1}]
action.calculer=Calculer
erreur.aleatoire=erreur aléatoire
resultats=Résultats
resultats.erreur=Une erreur s''est produite : [{0}]
resultats.titre=Résultats
message.zone=Nombre d'accès :
[messages_en.properties]
NotNull=Required field
typeMismatch=Invalid format
actionModel01.a.min=The number must be greater or equal to 0
DecimalMin.actionModel01.a=The number must be greater or equal to 0
DecimalMax.actionModel01.b=The number must be greater or equal to 0
actionModel01.b.min=The number must be greater or equal to 0
valeur.a=A value
valeur.b=B value
actionModel01.a.min.value=0
actionModel01.b.min.value=0
labelHeureCalcul=Computing hour:
LabelErreur=There was an error: [{0}]
labelAplusB=A+B=
labelAmoinsB=A-B=
labelAfoisB=A*B=
labelAdivB=A/B=
titre.part1=Arithmetic operations on two positive or equal to zero real numbers
labelHeureGetCulture=Loading hour: [{0}], culture: [{1}]
action.calculer=Calculate
erreur.aleatoire=randomly generated error
resultats=Results
resultats.erreur=Some error occurred : [{0}]
resultats.titre=Results
message.zone=Number of hits:
Ora esaminiamo gli attributi del tag [form]:
<form id="formulaire" name="formulaire" method="post" data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading" action="/ajax-02.html">
Si riconoscono gli attributi classici del tag [form]:
<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">
Si nota subito che se nel browser che visualizza la pagina JavaScript è disattivato, il modulo verrà inviato a URL [/ajax-02.html]. Analizziamo ora gli altri attributi:
<form ... data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading">
Gli attributi [data-ajax-xxx] sono gestiti dalla libreria jS [unobtrusive-ajax] che è stata importata dalla vista [vue-01.xml]:
<script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>
Quando sono presenti gli attributi [data-ajax-xxx], il [submit] del modulo verrà eseguito tramite una chiamata Ajax dalla libreria [unobtrusive-ajax]. Il significato dei parametri è il seguente:
- [data-ajax="true"]: è la presenza di questo attributo che fa sì che il [submit] del modulo venga trasformato in Ajax;
- [data-ajax-method="post"]: il metodo del [submit]. Il URL del POST sarà quello dell’attributo [action="/ajax-02.html"];
- [data-ajax-loading="#loading"]: l'ID di un campo da visualizzare in attesa della risposta del server. Il campo identificato da [loading] nella vista [vue-01.xml] è il seguente:
<img id="loading" style="display: none" src="/images/loading.gif" />
Si tratta di un'immagine animata di attesa che verrà visualizzata finché non sarà stata ricevuta la risposta del server;
- [data-ajax-loading-duration="0"]: il tempo di attesa in millisecondi prima che venga visualizzata l’area [data-ajax-loading="#loading"]. In questo caso, verrà visualizzata non appena inizierà l’attesa;
- [data-ajax-begin="beforeSend"]: la funzione jS da eseguire prima di eseguire la funzione [submit];
- [data-ajax-complete="afterComplete"]: la funzione jS da eseguire una volta ricevuta la risposta;
- [data-ajax-update="#resultats"]: l'identificativo dell'area in cui verrà inserito il risultato inviato dal server. La vista [vue-01.xml] contiene la seguente area:
<div id="resultats" />
- [data-ajax-mode="replace"]: la modalità di inserimento del risultato nell'area precedente. La modalità [replace] farà sì che il risultato "sovrascriva" ciò che era presente in precedenza nell'area con ID [resultats];
Va notato che il codice JavaScript [submit] verrà eseguito solo se i validatori hanno dichiarato validi i valori testati.
La libreria jS [unobtrusive-ajax] ha due obiettivi:
- garantire che il modulo si adatti correttamente alle due possibilità: attivazione o meno di JavaScript sul browser;
- evitare di scrivere codice JavaScript. Vedremo che in questo caso non è stato possibile evitarlo.
7.2.5. L'azione [/ajax-02]
Abbiamo visto che i valori inviati tramite POST venivano trasmessi all’azione [/ajax-02]. Questa è la seguente:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) throws InterruptedException {
// tempo?
Integer tempo = (Integer) session.getAttribute("tempo");
if (tempo != null && tempo > 0) {
Thread.sleep(tempo);
}
// si prepara il modello della vista successiva
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
// si imposta l'ora locale
setLocale(locale, modèle, résultats);
// ora
résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
...
}
- Per ora semplifichiamo: supponiamo che l’azione POST sia stata effettivamente eseguita dal JavaScript della vista [vue-01.xml]. Torneremo su questa ipotesi più avanti;
- riga 2: i valori [a,b] inviati vengono inseriti nel modello [ActionModel01];
- righe 4-7: se l’utente aveva impostato un tempo di attesa in un precedente GET, questo viene recuperato dalla sessione e viene applicato il tempo di attesa (riga 6). Lo scopo di tale temporizzazione è consentire all’utente di vedere l’effetto dell’attributo [data-ajax-loading="#loading"] nel modulo;
- righe 9-10: si inserisce un attributo [resultats] nel modello;
- riga 12: si inserisce la cultura [fr-FR] o [en-US] nel modello;
- riga 14: inserire l'ora POST nel modello;
Ricordiamo il tipo [Resultats] inserito nel modello:
public class Resultats {
// dati
private String aplusb;
private String amoinsb;
private String amultiplieparb;
private String adiviseparb;
private String heureGet;
private String heurePost;
private String erreur;
private String vue;
private String culture;
// getter e setter
...
}
Il codice dell'azione [/ajax-02] prosegue come segue:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session) throws InterruptedException {
...
résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
// si genera un errore una volta su due
int val = new Random().nextInt(2);
if (val == 0) {
// si restituisce un messaggio di errore
résultats.setErreur("erreur.aleatoire");
return "vue-03";
}
...
}
- righe 6-11: nell’esempio si mostra come restituire una pagina di errore al cliente jS. Una volta su due, viene restituita la seguente vista [vue-03.xml]:
![]() |
Si noti, alla riga 9, che non si tratta di un messaggio inserito nel modello, bensì di una chiave di messaggio:
[messages_fr.properties]
erreur.aleatoire=erreur aléatoire
[messages_fr.properties]
erreur.aleatoire=randomly generated error
Il codice della vista [vue-03.xml] è il seguente:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h4>Résultats</h4>
<p>
<strong>
<span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
<span id="heureCalcul" th:text="${resultats.heurePost}"></span>
</strong>
</p>
<p style="color: red;">
<span th:text="#{LabelErreur(#{${resultats.erreur}})}">Une erreur s'est produite :</span>
<!-- <span id="errore" th:text="${resultats.erreur}"></span> -->
</p>
</body>
</html>
- alla riga 12 si nota un messaggio configurato da una chiave di messaggio che a sua volta è calcolata. Abbiamo introdotto questo concetto al paragrafo 5.18, pagina 170.
Il codice dell'azione [/ajax-02] prosegue così:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session) throws InterruptedException {
...
// si recuperano i valori inviati
double a = formulaire.getA();
double b = formulaire.getB();
// si costruisce il modello
résultats.setAplusb(String.valueOf(a + b));
résultats.setAmoinsb(String.valueOf(a - b));
résultats.setAmultiplieparb(String.valueOf(a * b));
try {
résultats.setAdiviseparb(String.valueOf(a / b));
} catch (RuntimeException e) {
résultats.setAdiviseparb("NaN");
}
// si visualizza la vista
return "vue-02";
}
- righe 5-15: le quattro operazioni aritmetiche vengono eseguite sui numeri [a,b] e incapsulate nell’istanza [Resultats] del modello;
- riga 17: viene restituita la vista [vue-02.xml] seguente:
![]() |
La vista [vue-02.xml] è la seguente:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h4>Résultats</h4>
<p>
<strong>
<span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
<span id="heureCalcul" th:text="${resultats.heurePost}"></span>
</strong>
</p>
<p>
<span th:text="#{labelAplusB}">A+B=</span>
<span id="aplusb" th:text="${resultats.aplusb}"></span>
</p>
<p>
<span th:text="#{labelAmoinsB}">A-B=</span>
<span id="amoinsb" th:text="${resultats.amoinsb}"></span>
</p>
<p>
<span th:text="#{labelAfoisB}">A*B=</span>
<span id="amultiplieparb" th:text="${resultats.amultiplieparb}"></span>
</p>
<p>
<span th:text="#{labelAdivB}">A/B=</span>
<span id="adiviseparb" th:text="${resultats.adiviseparb}"></span>
</p>
</body>
</html>
Che il risultato sia la vista [vue-02.xml] o la vista [vue-03.xml], questo risultato HTML viene inserito nel campo identificato da [resultats] nella vista [vue-01.xml], a causa dell'attributo [data-ajax-update="#resultats"] del modulo.
7.2.6. Il POST dei valori inseriti
Qui si presenta una difficoltà con i valori inseriti. Si lavora con due culture, [fr-FR] e [en-US], che scrivono i numeri reali in modo diverso. Avevamo già affrontato questa difficoltà quando, nel paragrafo 6.3 a pagina 190, era stato necessario inserire numeri reali in due culture diverse. Riprenderemo qui gli strumenti utilizzati in quell’occasione. Tuttavia, abbiamo un’ulteriore difficoltà: non abbiamo accesso al metodo che esegue la conversione POST dei valori inseriti. Per questo motivo, abbiamo aggiunto i seguenti attributi al tag del modulo:
- [data-ajax-begin="beforeSend"]: la funzione jS da eseguire prima di eseguire la funzione [submit];
- [data-ajax-complete="afterComplete"]: la funzione jS da eseguire una volta ricevuta la risposta;
Non abbiamo accesso alla funzione jS che invierà i valori inseriti, ma possiamo scrivere due funzioni jS:
- [beforeSend]: una funzione jS eseguita prima della POST;
- [afterComplete]: una funzione jS eseguita al ricevimento della risposta alla funzione POST;
Queste due funzioni sono contenute in un file [local1.js]:
![]() |
Il file [local1.js] inizializza l'ambiente jS della vista [vue-01.xml] nel modo seguente:
// dati globali
var loading;
var formulaire;
var résultats;
var a, b;
// al caricamento del documento
$(document).ready(function() {
// si recuperano i riferimenti dei diversi componenti della pagina
loading = $("#loading");
formulaire = $("#formulaire");
resultats = $('#risultati');
a = $("#a");
b = $("#b");
// si nascondono alcuni elementi
loading.hide();
// si analizzano i validatori del modulo
$.validator.unobtrusive.parse(formulaire);
// si gestiscono due stringhe di localizzazione [fr_FR, en_US]
// i valori effettivi [a,b] vengono inviati dal server in formato anglosassone
// se necessario, li si converte nel formato francese
checkCulture(2);
});
- riga 22: la funzione [checkCulture] viene presentata più avanti;
La funzione jS [beforeSend] sarà la seguente:
function beforeSend(jqXHR, settings) {
// prima di POST
// i numeri devono essere inseriti nel formato anglosassone
var culture = Globalize.culture().name;
if (culture === 'fr-FR') {
checkCulture(1);
settings.data = formulaire.serialize();
}
}
function afterComplete(jqXHR, settings) {
...
}
function checkCulture(mode) {
if (mode == 1) {
// i numeri [a,b] vanno convertiti nel formato anglosassone
var value1 = a.val().replace(",", ".");
a.val(value1);
var value2 = b.val().replace(",", ".");
b.val(value2);
}
if (mode == 2) {
...
}
}
- righe 4-6: si verifica se la cultura della vista è [fr-FR]. In questo caso, è necessario modificare i valori inviati. Infatti, se l'utente ha inserito [1,6], occorre inviare il valore [1.6]; in caso contrario, il valore [1,6] verrà rifiutato dal server. A tal fine è sufficiente sostituire la virgola nei valori inviati con il punto decimale (righe 18-21);
- ma non ci si può fermare qui. Infatti, quando viene chiamata la funzione [beforeSend], la stringa dei valori inviati [a=val1&b=valB] è già stata costruita. Dobbiamo quindi modificarla. Ciò avviene tramite il secondo parametro [settings] della funzione;
- riga 7: [settings.data] (settings è un parametro della funzione) rappresenta la stringa inviata. Ricreiamo questa stringa con l'espressione [formulaire.serialize()]. Questa espressione analizza il modulo alla ricerca dei valori da inviare e costruisce la stringa di POST. Prenderà quindi i nuovi valori di [a,b] con i punti decimali;
Se non si interviene ulteriormente, il server invierà la sua risposta, che verrà visualizzata correttamente. Tuttavia, a questo punto i valori di [a,b] presentano il punto decimale, mentre ci si trova ancora nella cultura [fr-FR]. Se quindi l’utente non se ne accorge e clicca nuovamente su [Calculer], i validatori gli rispondono che i valori [a,b] non sono validi. Il che è corretto. È qui che interviene la funzione [afterComplete], eseguita al ricevimento del risultato:
function beforeSend(jqXHR, settings) {
// prima di POST
...
}
function afterComplete(jqXHR, settings) {
// dopo il POST
// i numeri devono essere riportati nel formato francese, se necessario
var culture = Globalize.culture().name;
if (culture === 'fr-FR') {
checkCulture(2);
}
}
function checkCulture(mode) {
if (mode == 1) {
...
}
if (mode == 2) {
// si convertono i numeri nel formato francese
var value1 = a.val().replace(".", ",");
a.val(value1);
var value2 = b.val().replace(".", ",");
b.val(value2);
}
}
- righe 9-12: se la lingua della vista è [fr-FR], si riportano i numeri [a,b] al formato francese.
7.2.7. Test
Ecco alcune schermate dei test:
![]() |
- in [1], la risposta del server;
![]() |
- in [2], la risposta del server con un messaggio di errore;
![]() |
- in [3], si imposta un tempo di attesa di 5 secondi. Ciò significa che il server attenderà 5 secondi prima di inviare la propria risposta. Nel tag [form], abbiamo utilizzato l'attributo [data-ajax-loading='#loading']. Il parametro [loading] è l'identificatore di un'area che viene:
- visualizzata per tutta la durata dell'attesa;
- nascosta dopo la ricezione della risposta dal server;
In questo caso, [loading] è l’identificatore di un’immagine animata visibile in [4].
7.2.8. Disattivazione di JavaScript con l'elemento [en-US]
Cosa succede se si disattiva JavaScript nel browser?
La POST dei valori inseriti avverrà in base al tag [form], i cui attributi [data-ajax-attr] non verranno utilizzati. Tutto avviene come se si avesse il seguente tag [form]:
<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">
I valori inseriti verranno quindi inviati all'azione [/ajax-02]. Non saranno stati verificati sul lato client. Saranno quindi i validatori sul lato server a intervenire. Intervenivano già in precedenza, ma su valori già convalidati sul lato client, quindi corretti. Ora non è più così.
Modifichiamo l’azione [/ajax-02] nel modo seguente:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
// richiesta Ajax?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
}
- riga 4: l’azione [/ajax-02] può quindi ora essere richiamata tramite un POST Ajax o tramite un POST classico. Dobbiamo essere in grado di distinguere questi due casi. Lo si fa grazie alle intestazioni HTTP inviate dal browser client;
osservando il traffico di rete nella console di sviluppo di Chrome (Ctrl-Maiusc-I) con JavaScript abilitato, si nota che il client invia le seguenti intestazioni al momento dell’esecuzione di POST:
![]() |
Come si vede sopra:
- è stata inviata un'intestazione [X-Requested-With] [1];
- un parametro [X-Requested-With] è stato aggiunto ai valori inviati [2];
Ciò non avviene nel caso di un POST classico. Abbiamo quindi due possibilità per recuperare l’informazione: recuperarla nelle intestazioni HTTP o nei valori inviati. La riga 4 dell'azione [/ajax-02] ha scelto la prima soluzione.
Proseguiamo con il codice di questa azione:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
// richiesta Ajax?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
// tempo?
Integer tempo = (Integer) session.getAttribute("tempo");
if (tempo != null && tempo > 0) {
Thread.sleep(tempo);
}
// si prepara il modello della vista successiva
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
// si imposta l'impostazione locale
setLocale(locale, modèle, résultats);
// ora
String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
résultats.setHeurePost(heure);
résultats.setHeureGet(heure);
// richiesta valida?
if (!isAjax && result.hasErrors()) {
return "vue-01";
}
...
- riga 2: il parametro [@Valid ActionModel01 formulaire] attiva i validatori lato server;
- righe 20-22: se la chiamata non è di tipo Ajax e la convalida non va a buon fine, viene restituita la vista [vue-01.xml] con i messaggi di errore.
Ecco un esempio:
![]() | ![]() |
Continuiamo l’analisi dell’azione [/ajax-02]:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
// richiesta Ajax?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
// Richiesta valida?
if (!isAjax && result.hasErrors()) {
return "vue-01";
}
// si genera un errore una volta su due
int val = new Random().nextInt(2);
if (val == 0) {
// viene restituito un messaggio di errore
résultats.setErreur("erreur.aleatoire");
if (isAjax) {
return "vue-03";
} else {
résultats.setVue("vue-03");
return "vue-01";
}
}
...
- riga 14: viene generato un errore casuale;
- riga 16: in caso di chiamata Ajax, viene restituita la vista [vue-03.xml] che verrà inserita nell'area identificata da [resultats];
- riga 18: in caso di chiamata non Ajax, si inserisce la vista da visualizzare nel modello di tipo [Resultats];
- riga 19: si restituisce nuovamente la vista [vue-01.xml];
La vista [vue-01.xml] viene modificata come segue:
<div id="resultats" />
<div th:if="${resultats.vue}=='vue-02'" th:include="vue-02" />
<div th:if="${resultats.vue}=='vue-03'" th:include="vue-03" />
- riga 3: la vista [vue-03.xml] verrà inserita sotto l'area [resultats];
Ecco un esempio:
![]() |
Si noti che ora le ore [1] e [2] sono identiche.
Continuiamo l’analisi dell’azione [/ajax-02]:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
// richiesta Ajax?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
// si recuperano i valori inviati
double a = formulaire.getA();
double b = formulaire.getB();
// si costruisce il modello
résultats.setAplusb(String.valueOf(a + b));
résultats.setAmoinsb(String.valueOf(a - b));
résultats.setAmultiplieparb(String.valueOf(a * b));
try {
résultats.setAdiviseparb(String.valueOf(a / b));
} catch (RuntimeException e) {
résultats.setAdiviseparb("NaN");
}
// visualizzazione della vista
if (isAjax) {
return "vue-02";
} else {
résultats.setVue("vue-02");
return "vue-01";
}
}
- righe 7-17: i risultati delle quattro operazioni aritmetiche vengono inseriti nel modello;
- righe 22-23: si genera la vista [vue-01.xml] (riga 22) inserendovi la vista [vue-02.xml] (riga 22);
Tale inserimento avviene nel modo seguente in [vue-01.xml]:
<div id="resultats" />
<div th:if="${resultats.vue}=='vue-02'" th:include="vue-02" />
<div th:if="${resultats.vue}=='vue-03'" th:include="vue-03" />
- riga 2: la vista [vue-02.xml] verrà inserita sotto l'area [resultats];
Ecco un esempio di esecuzione:
![]() |
7.2.9. Disattivazione di JavaScript con la cultura [fr-FR]
Con la cultura [fr-FR] si verifica il seguente problema:
![]() | ![]() |
I valori inseriti nel formato francese sono stati dichiarati non validi. Infatti, il server si aspetta valori reali nel formato anglosassone. La soluzione è piuttosto complessa. Creeremo un filtro che:
- intercetti la richiesta;
- sostituirà le virgole nei valori inviati [a] e [b] con il punto decimale;
- quindi inoltrare la nuova richiesta all’azione che deve elaborarla;
Per prima cosa, inseriamo un campo nascosto nella vista [vue-01.xml]:
<form ...>
...
</p>
<!-- campi nascosti -->
<input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
- riga 5: il valore [fr-FR] o [en-US] viene inserito nel campo attributo [name=culture]. Poiché il tag [input] è presente nel modulo, il suo valore verrà inviato insieme ai valori di [a] e [b]. Si otterrà quindi una stringa inviata della forma:
È importante comprendere questo punto.
Successivamente includiamo un filtro nella configurazione dell’applicazione:
![]() |
Il file [Config] viene modificato come segue:
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
@Bean
public Filter cultureFilter() {
return new CultureFilter();
}
}
- riga 7: il fatto che il bean [cultureFilter] restituisca un tipo [Filter] lo rende un filtro. Il bean, dal canto suo, può avere un nome qualsiasi;
Il passo successivo consiste nel creare il filtro stesso:
![]() |
package istia.st.springmvc.config;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;
public class CultureFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// handler successivo
filterChain.doFilter(new CultureRequestWrapper(request), response);
}
}
- riga 12: estendiamo la classe [OncePerRequestFilter], che è una classe Spring, e ciò che dobbiamo fare è ridefinire il metodo [doFilterInternal] di questa classe;
- riga 15: il metodo [doFilterInternal] riceve tre informazioni:
- [HttpServletRequest request]: la richiesta da filtrare. Questa non può essere modificata,
- [HttpServletResponse response]: la risposta che verrà inviata al server. Il filtro può decidere di generarla autonomamente,
- [FilterChain filterChain]: la catena dei filtri. Una volta che il metodo [doFilterInternal] ha terminato il proprio lavoro, deve passare la richiesta al filtro successivo della catena dei filtri;
- riga 18: si crea una nuova richiesta a partire da quella ricevuta [new CultureRequestWrapper(request)] e la si passa al filtro successivo. Poiché non è possibile modificare la richiesta iniziale [HttpServletRequest request], se ne crea una nuova;
La classe [CultureRequestWrapper] è la seguente:
![]() |
package istia.st.springmvc.config;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class CultureRequestWrapper extends HttpServletRequestWrapper {
public CultureRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String[] getParameterValues(String name) {
// valori inviati a e b
if (name != null && (name.equals("a") || name.equals("b"))) {
String[] values = super.getParameterValues(name);
String[] newValues = values.clone();
newValues[0] = newValues[0].replace(",", ".");
return newValues;
}
// altri casi
return super.getParameterValues(name);
}
}
- riga 6: la classe [CultureRequestWrapper] estende la classe [HttpServletRequestWrapper] e ridefinirà alcuni dei suoi metodi;
- righe 8-10: il costruttore che riceve la richiesta da filtrare e la passa alla classe padre;
- è importante comprendere che la richiesta filtrata finirà per diventare un parametro di input di una classe denominata servlet. Con Spring MVC, questa servlet è di tipo [DispatcherServlet]. Questa classe dispone di vari metodi per recuperare i parametri della richiesta: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. È necessario ridefinire il metodo utilizzato dalla servlet. A tal fine, bisognerebbe leggere il codice della classe [DispatcherServlet]. Io non l’ho fatto e ho ridefinito diversi metodi. Alla fine è stato ridefinito il metodo [getParameterValues];
- riga 13: il metodo [getParameterValues] riceve come parametro il nome di uno dei parametri restituiti dal metodo [getParameterNames] e deve restituire l’array dei relativi valori. Infatti, è noto che un parametro può essere presente più volte in una richiesta;
- riga 18: si sostituisce la virgola con un punto decimale;
Ecco un esempio di esecuzione:
![]() |
- in [1], i valori [a,b] sono inseriti nel formato francese;
- in [2], i risultati;
- in [3], il server ha restituito una pagina con numeri in formato anglosassone.
Quest’ultimo problema può essere risolto con Thymeleaf nel modo seguente nella vista [vue-01.xml]
<tr>
<td>
<input type="text" id="a" name="a" th:value="${resultats.culture}=='fr-FR' and ${actionModel01.a}!=null? ${#strings.replace(actionModel01.a,'.',',')} : ${actionModel01.a}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.a.min},data-val-min-value=#{actionModel01.a.min.value}" />
</td>
<td>
<input type="text" id="b" name="b" th:value="${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null? ${#strings.replace(actionModel01.b,'.',',')} : ${actionModel01.b}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.b.min},data-val-min-value=#{actionModel01.b.min.value}" />
</td>
</tr>
Ci sono diverse modifiche da apportare alle righe 3 e 6. Analizziamo la riga 3:
- avevamo scritto [th:field="*{a}"]. Il parametro [th:field] imposta gli attributi [id, name, value] del tag HTML [input] generato. In questo caso, vogliamo gestire noi stessi l’attributo [value]. Impostiamo quindi anche noi stessi gli attributi [id, name];
- l’attributo [th:value] valuta un’espressione che utilizza l’operatore ternario ?. Si verifica l’espressione [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]. Se è vera, si assegna all’attributo [value] il valore di [actionModel01.a], dove il punto decimale è sostituito dalla virgola. Se è falsa, all’attributo [value] viene assegnato il valore di [actionModel01.a] senza modifiche;
- riga 6: si ripete la stessa operazione per il campo [b];
Ecco un esempio di esecuzione:
![]() |
- in [1], i numeri [a,b] hanno mantenuto la notazione francese. Non è così in [2];
Questo nuovo problema si risolve allo stesso modo del precedente. Si modifica la vista [vue-03.xml] nel modo seguente:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h4 th:text="#{resultats}">Résultats</h4>
<p>
<strong>
<span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
<span id="heureCalcul" th:text="${resultats.heurePost}"></span>
</strong>
</p>
<p>
<span th:text="#{labelAplusB}">A+B=</span>
<span id="aplusb" th:text="${resultats.culture}=='fr-FR' and ${resultats.aplusb}!=null? ${#strings.replace(resultats.aplusb,'.',',')} : ${resultats.aplusb}"></span>
</p>
<p>
<span th:text="#{labelAmoinsB}">A-B=</span>
<span id="amoinsb" th:text="${resultats.culture}=='fr-FR' and ${resultats.amoinsb}!=null? ${#strings.replace(resultats.amoinsb,'.',',')} : ${resultats.amoinsb}"></span>
</p>
<p>
<span th:text="#{labelAfoisB}">A*B=</span>
<span id="amultiplieparb" th:text="${resultats.culture}=='fr-FR' and ${resultats.amultiplieparb}!=null? ${#strings.replace(resultats.amultiplieparb,'.',',')} : ${resultats.amultiplieparb}"></span>
</p>
<p>
<span th:text="#{labelAdivB}">A/B=</span>
<span id="adiviseparb" th:text="${resultats.culture}=='fr-FR' and ${resultats.adiviseparb}!=null? ${#strings.replace(resultats.adiviseparb,'.',',')} : ${resultats.adiviseparb}"></span>
</p>
</body>
</html>
Ecco un esempio:
![]() | ![]() |
Ora disponiamo di un'applicazione che gestisce correttamente due culture in un ambiente che utilizza o meno JavaScript. A tal fine è stato necessario complicare notevolmente il codice lato server. D'ora in poi, daremo sempre per scontato che JavaScript sia abilitato nel browser. Ciò consente di realizzare operazioni impossibili in modalità esclusivamente server.
7.2.10. Gestione del link [Calculer]
Esaminiamo il link [Calculer] della pagina principale [vue-01.xml]:
![]() | ![]() |
Il codice del link [Calculer] nella vista [vue-01.xml] è il seguente:
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
La funzione jS [postForm] è definita nel file [local1.js] come segue:
// dati globali
var loading;
var formulaire;
var résultats;
var a, b;
function postForm() {
// modulo valido?
if (!formulaire.validate().form()) {
// modulo non valido - terminato
return;
}
// si gestiscono due impostazioni locali [fr_FR, en_US]
// i dati effettivi [a,b] devono essere inviati in formato anglosassone in ogni caso
// saranno gestiti dal filtro [CultureFilter]
// si effettua manualmente una chiamata Ajax
$.ajax({
url : '/ajax-02',
headers : {
'«X-Requested-With»: «XMLHttpRequest»
},
type : 'POST',
data : formulaire.serialize(),
dataType : 'html',
beforeSend : function() {
loading.show();
},
success : function(data) {
resultats.html(data);
},
complete : function() {
loading.hide();
},
error : function(jqXHR) {
résultats.html(jqXHR.responseText);
}
})
}
- righe 2-5: ricordiamo che questi elementi sono stati inizializzati dalla funzione [$(document).ready];
- righe 9-12: vengono eseguiti i validatori jS del modulo. Se uno dei valori non è valido, l'espressione [formulaire.validate().form()] restituisce il valore false. In questo caso, il validatore [submit] del modulo viene annullato;
- righe 18-38: si effettua manualmente una chiamata Ajax;
- riga 19: l’URL di destinazione della chiamata Ajax;
- righe 20-22: un array di intestazioni HTTP da aggiungere a quelle presenti di default nella richiesta HTTP. Qui si aggiunge l'intestazione HTTP che indicherà al server che si sta effettuando una chiamata Ajax;
- riga 23: il metodo HTTP utilizzato;
- riga 24: i dati inviati via POST. [formulaire.serialize] crea la stringa da inviare via POST [culture=fr-FR&a=12,7&b=20,89] del modulo con ID [formulaire]. Qui ritroviamo il problema esaminato in precedenza: i valori [a,b] devono essere inviati nel formato anglosassone. Sappiamo che questo problema è stato ormai risolto con la creazione del filtro [cultureFilter];
- riga 25: il tipo di dati atteso in risposta. Sappiamo che il server restituirà un flusso HTML;
- riga 26: il metodo da eseguire all’avvio della richiesta. Qui si indica che occorre visualizzare il componente con id [loading]. Si tratta dell’immagine animata di attesa;
- riga 29: il metodo da eseguire in caso di esito positivo della richiesta Ajax. Il parametro [data] è la risposta completa del server. Sappiamo che si tratta di un flusso HTML;
- riga 30: si aggiorna il componente con id [résultats] con il valore HTML del parametro [data].
- riga 33: si nasconde il segnale di attesa;
- riga 35: funzione eseguita quando viene ricevuta la risposta del server, indipendentemente dal fatto che si tratti di un esito positivo o di un errore;
- righe 35-37: in caso di errore (il server ha restituito una risposta HTTP con uno stato che indica la presenza di un errore lato server), viene visualizzata la risposta HTML del server nell'area [resultats];
Ecco un esempio di esecuzione:
![]() | ![]() |
7.3. Aggiornamento di una pagina HTML con un flusso jSON
Nell'esempio precedente, il server web rispondeva alla richiesta Ajax HTTP con un flusso HTML. In questo flusso erano presenti dati accompagnati dalla formattazione HTML. Ci proponiamo di riprendere l’esempio precedente, questa volta con risposte jSON (JavaScript Object Notation) contenenti solo i dati. Il vantaggio è che in questo modo si trasmettono meno byte. Si suppone che JavaScript sia abilitato sul browser.
7.3.1. L'azione [/ajax-04]
L'azione [/ajax-04] è identica all'azione [/ajax-01], tranne per il fatto che viene visualizzata la vista [vue-04.xml] invece della vista [vue-01.xml]:
@RequestMapping(value = "/ajax-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax04(Locale locale, Model modèle, HttpSession session, String tempo) {
...
// vista
return "vue-04";
}
7.3.2. La vista [vue-04.xml]
![]() |
La vista [vue-04.xml] riprende il corpo della vista [vue-01.xml] con le seguenti differenze:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
...
<script type="text/javascript" src="/js/local4.js"></script>
<script th:inline="javascript">
/*<![CDATA[*/
var culture = [[${resultats.culture}]];
Globalize.culture(culture);
/*]]>*/
</script>
</head>
<body>
<h2>Ajax - 04</h2>
...
<form id="formulaire" name="formulaire" th:object="${actionModel01}">
...
<p>
<img id="loading" style="display: none" src="/images/loading.gif" />
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
</p>
<!-- campi nascosti -->
<input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
<hr />
<div id="entete">
<h4 id="titre">Résultats</h4>
<p>
<strong>
<span id="labelHeureCalcul">Heure de calcul :</span>
<span id="heureCalcul">12:10:87</span>
</strong>
</p>
</div>
<div id="résultats">
<p>
A+B=
<span id="aplusb">16,7</span>
</p>
<p>
A-B=
<span id="amoinsb">16,7</span>
</p>
<p>
A*B=
<span id="afoisb">16,7</span>
</p>
<p>
A/B=
<span id="adivb">16,7</span>
</p>
</div>
<div id="erreur">
<p style="color: red;">
<span id="msgErreur">xx</span>
</p>
</div>
</body>
</html>
- riga 5: il codice JavaScript della vista si trova ora nel file [local4.js];
- riga 16: il tag [form] non presenta più i parametri [data-ajax-attr] della libreria [Unobtrusive Ajax]. Non lo utilizzeremo in questa sede. Anche il tag [form] non presenta gli attributi [method] e [action] che indicano come e dove inviare i valori inseriti nel modulo. Questo perché verrà inviato tramite la funzione jS (riga 20);
- righe 26-57: il campo ID [resultats], che in precedenza era vuoto, ora contiene il codice HTML per visualizzare i risultati;
- righe 26-34: l'intestazione dei risultati in cui viene visualizzata l'ora del calcolo;
- righe 35-52: i risultati delle quattro operazioni aritmetiche;
- righe 53-57: un eventuale messaggio di errore inviato dal server;
Il codice jS eseguito al caricamento della vista [vue-04.xm] si trova nel file [local4.js]. È il seguente:
// dati globali
var loading;
var formulaire;
var résultats;
var titre;
var labelHeureCalcul;
var heureCalcul;
var aplusb;
var amoinsb;
var afoisb;
var adivb;
var msgErreur;
// al caricamento del documento
$(document).ready(function() {
// si recuperano i riferimenti dei diversi componenti della pagina
loading = $("#loading");
formulaire = $("#formulaire");
résultats = $('#risultati');
titre=$("#titre");
labelHeureCalcul=$("#labelHeureCalcul");
heureCalcul=$("#heureCalcul");
aplusb=$("#aplusb");
amoinsb=$("#amoinsb");
afoisb=$("#afoisb");
adivb=$("#adivb");
msgErreur=$("#msgErreur");
// si nascondono alcuni elementi
résultats.hide();
erreur.hide();
loading.hide();
});
- righe 17-27: si recuperano i riferimenti jQuery di tutti gli elementi della pagina;
- riga 29: l'area dei risultati viene nascosta;
- riga 30: così come l’area dell’errore;
- riga 31: così come l'immagine animata di attesa;
- righe 2-12: i riferimenti recuperati vengono definiti globali in modo che le altre funzioni possano utilizzarli;
7.3.3. La funzione jS [postForm]
Il collegamento [Calculer] è il seguente:
<p>
<img id="loading" style="display: none" src="/images/loading.gif" />
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
</p>
La funzione jS [postForm] è definita nel file [local.js] come segue:
function postForm() {
// modulo valido?
if (!formulaire.validate().form()) {
// modulo non valido - terminato
return;
}
// si effettua manualmente una richiesta Ajax
$.ajax({
url : '/ajax-05',
headers : {
'Accept: 'application/json'
},
type : 'POST',
data : formulaire.serialize(),
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
// prima della chiamata Ajax
function onBegin() {
...
}
// alla ricezione della risposta dal server
// in caso di esito positivo
function onSuccess(data) {
...
}
// alla ricezione della risposta dal server
// in caso di errore
function onError(jqXHR) {
...
}
// dopo [onSuccess, onError]
function onComplete() {
...
}
- righe 3-6: prima di inviare i valori inseriti, questi vengono verificati. Se non sono corretti, non viene eseguita l’azione POST del modulo;
- riga 9: i valori inseriti vengono inviati all’azione [/ajax-05], che descriveremo più dettagliatamente in seguito;
- righe 10-12: un'intestazione HTTP per indicare al server che ci si aspetta una risposta nel formato jSON;
- riga 13: i valori inseriti verranno inviati;
- riga 14: serializzazione dei valori inseriti in una stringa pronta per essere inviata [a=1,6&b=2,4&culture=fr-FR];
- riga 15: il tipo di risposta inviata dal server. Sarà jSON;
- riga 16: la funzione da eseguire prima di POST;
- riga 17: la funzione da eseguire alla ricezione della risposta del server, se questa ha esito positivo. Il «successo» di una richiesta HTTP viene valutato in base allo stato della risposta HTTP del server. Una risposta [HTTP/1.1 200 OK ] è una risposta di successo. Una risposta [HTTP/1.1 500 Internal Server Error] è una risposta di errore. Lo stato di una risposta HTTP è rappresentato dal codice [200] o [500]. Alcuni di questi codici sono associati al «successo», mentre altri sono associati al «fallimento»;
- riga 18: la funzione da eseguire alla ricezione della risposta del server quando lo stato HTTP di tale risposta indica un errore;
- riga 18: la funzione da eseguire per ultima, dopo le precedenti funzioni [onSuccess, onError];
La funzione [onBegin] è la seguente:
// prima della chiamata Ajax
function onBegin() {
console.log("onBegin");
// si visualizza l'immagine animata
loading.show();
// si nascondono alcuni elementi della vista
entete.hide();
résultats.hide();
erreur.hide();
}
Prima di esaminare le altre funzioni jS della chiamata Ajax, è necessario conoscere la risposta inviata dall’azione [/ajax-05].
7.3.4. L’azione [/ajax-05]
L’azione [/ajax-05] è la seguente:
@RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
@ResponseBody()
// elabora il POST della vista [vue-04]
public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, HttpServletRequest request, HttpSession session) throws InterruptedException {
if(result.hasErrors()){
// caso anomalo - non viene visualizzato nulla
return null;
}
...
}
- riga 2: l'attributo [ResponseBody] indica che l'azione [/ajax-05] restituisce direttamente la risposta al client. Poiché una libreria jSON è presente tra le dipendenze del progetto, Spring Boot configura automaticamente questo tipo di azioni in modo che restituiscano jSON. Sarà quindi la stringa jSON di tipo [JsonResults] (riga 4) a essere inviata al client;
- riga 2: i valori inviati [a, b, culture] verranno incapsulati in un tipo [ActionModel01] di cui si richiede la convalida [@Valid ActionModel01]. È solo una formalità. Si è partiti dall’ipotesi che JavaScript fosse abilitato sul browser del cliente e quindi, al loro arrivo, i valori inviati siano già stati verificati lato client. Tuttavia, è possibile prevedere il caso di un POST non autorizzato che non utilizzi il nostro client jS. In questo caso, la convalida potrebbe fallire;
- righe 5-7: in caso di errore, si restituisce un flusso jSON vuoto;
Continuiamo l’analisi dell’azione [/ajax-05]:
@RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
@ResponseBody()
// elabora il POST della vista [vue-04]
public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,
HttpServletRequest request, HttpSession session) throws InterruptedException {
...
// il contesto dell'applicazione Spring
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// tempo?
Integer tempo = (Integer) session.getAttribute("tempo");
if (tempo != null && tempo > 0) {
Thread.sleep(tempo);
}
...
// si restituisce il risultato
return résultats;
}
- riga 8: si recupera il contesto [ctx] dall’applicazione Spring. È necessario per recuperare i messaggi dai file [messages.properties] in base a una chiave di messaggio e a un’impostazione locale. Ciò avviene con la seguente sintassi:
ctx.getMessage(clé_message, tableau_de_paramètres, locale)
- [clé_message]: la chiave del messaggio ricercato;
- [locale]: la locale utilizzata. Pertanto, se questa locale è [en_US], verrà utilizzato il file [messages_en.properties];
- [tableau_de_paramètres]: il messaggio ottenuto può essere configurato come in [clé=message {0} {1}]. In questo messaggio sono presenti due parametri [{0} {1}]. Come secondo parametro di [ctx.getMessage] sarà necessario fornire un array di due valori;
- righe 10-13: se nella sessione è presente un tempo di attesa, il thread corrente viene sospeso per la durata di tale tempo di attesa;
L'azione [/ajax-05] prosegue come segue:
// si prepara il modello della vista successiva
JsonResults résultats = new JsonResults();
...
}
- riga 2: creazione del modello della stringa jSON inviata al cliente;
Il modello [JsonResults] è il seguente:
![]() |
package istia.st.springmvc.models;
public class JsonResults {
// dati
private String titre;
private String labelHeureCalcul;
private String heureCalcul;
private String aplusb;
private String amoinsb;
private String afoisb;
private String adivb;
private String msgErreur;
// getter e setter
...
}
- righe 6-13: ciascuno dei campi della classe [JsonResult] corrisponde a un campo dello stesso [id] nella vista [vue-04.xml]:
L'azione [/ajax-05] prosegue come segue:
// si prepara il modello della vista successiva
JsonResults résultats = new JsonResults();
// intestazione
résultats.setTitre(ctx.getMessage("resultats.titre", null, locale));
résultats.setLabelHeureCalcul(ctx.getMessage("labelHeureCalcul", null, locale));
résultats.setHeureCalcul(new SimpleDateFormat("hh:mm:ss").format(new Date()));
// si genera un errore una volta su due
int val = new Random().nextInt(2);
if (val == 0) {
// si restituisce un messaggio di errore
résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
return résultats;
}
- riga 2: creazione del modello della stringa jSON inviata al cliente;
- righe 4-6: si creano i messaggi dell’intestazione dei risultati;
- righe 8-14: in media una volta su due, viene generato un messaggio di errore. In questo caso, non si procede oltre e si restituisce la stringa jSON al cliente (riga 13);
- riga 11: ecco un esempio di messaggio personalizzato:
erreur.aleatoire=erreur aléatoire
resultats.erreur=Une erreur s''est produite : [{0}]
L'azione [/ajax-05] prosegue come segue:
// si recuperano i valori inviati
double a = formulaire.getA();
double b = formulaire.getB();
// si costruisce il modello
résultats.setAplusb(String.valueOf(a + b));
résultats.setAmoinsb(String.valueOf(a - b));
résultats.setAfoisb(String.valueOf(a * b));
try {
résultats.setAdivb(String.valueOf(a / b));
} catch (RuntimeException e) {
résultats.setAdivb("NaN");
}
// si restituisce il risultato
return résultats;
- righe 2-3: si recuperano i valori di [a] e [b];
- righe 5-12: si generano i quattro risultati;
- riga 14: la stringa jSON [JsonResults] viene inviata al client;
Vediamo cosa succede con il client [Advanced Rest Client]:
![]() |
- in [1-2], si effettua una richiesta POST all’azione [/ajax-05];
- in [3], si inviano valori errati;
- in [4], il server ha restituito un flusso vuoto;
![]() |
- in [1], vengono inviati valori corretti;
- in [2], l'oggetto jSON restituito dal server, con un messaggio di errore;
![]() |
- in [1], si inviano valori corretti;
- in [2], l'oggetto jSON restituito dal server, con i quattro risultati;
![]() |
- in [1], si inviano valori corretti;
- in [2], si è provveduto a provocare un'eccezione lato server. Si nota che il server invia ancora un oggetto jSON. In questo messaggio, si vede che lo stato HTTP della risposta è [500], a indicare che si è verificato un errore lato server;
7.3.5. La funzione jS [postForm] - 2
Ora che conosciamo l’oggetto jSON restituito dal server, possiamo utilizzarlo nel codice JavaScript. Il metodo [onSuccess] eseguito quando il server invia una risposta con lo stato HTTP [200] è il seguente:
// alla ricezione della risposta dal server
// in caso di esito positivo
function onSuccess(data) {
console.log("onSuccess");
// si compila l'area dei risultati
titre.text(data.titre);
labelHeureCalcul.text(data.labelHeureCalcul);
heureCalcul.text(data.heureCalcul);
entete.show();
// risultati senza errori
if (!data.msgErreur) {
aplusb.text(data.aplusb);
amoinsb.text(data.amoinsb);
afoisb.text(data.afoisb);
adivb.text(data.adivb);
résultats.show();
return;
}
// risultati con errore
msgErreur.text(data.msgErreur);
erreur.show();
}
- riga 3: il parametro [data] è l’oggetto jSON restituito dal server:
![]() |
Il metodo [onError] eseguito quando lo stato della risposta HTTP è [500] è il seguente:
// al ricevimento della risposta dal server
// in caso di errore
function onError(jqXHR) {
console.log("onError");
// errore di sistema
msgErreur.text(jqXHR.responseText);
erreur.show();
}
- riga 3: l'oggetto JQuery [jqXHR] presenta tra le sue proprietà le seguenti:
- responseText: il testo della risposta del server,
- status: il codice di errore restituito dal server,
- statusText: il testo associato a questo codice di errore;
- riga 6: l'oggetto [jqXHR.responseText] è il seguente oggetto jSON:
![]() |
7.3.6. Test
Vediamo alcuni screenshot dell'esecuzione dell'applicazione web:
![]() |
![]() |
![]() |
7.4. Applicazione web a pagina singola
7.4.1. Introduzione
La tecnologia Ajax consente di realizzare applicazioni a pagina singola:
- la prima pagina viene caricata tramite una richiesta classica del browser;
- le pagine successive vengono ottenute tramite chiamate Ajax. Pertanto, alla fine il browser non cambia mai pagina (URL) e non carica mai una nuova pagina. Questo tipo di applicazione viene chiamata Applicazione a Pagina Singola (APU) o, in inglese, Single Page Application (SPA).
Ecco un esempio semplice di tale applicazione. La nuova applicazione avrà due viste:
![]() |
![]() |
- in [1], l’azione [/ajax-06] ci permette di visualizzare la prima pagina, la pagina 1;
- in [2], un link ci permette di passare alla pagina 2 tramite una chiamata Ajax;
- in [3], l'URL non è cambiato. La pagina visualizzata è la pagina 2;
- in [4], un link ci permette di tornare alla pagina 1 tramite una chiamata Ajax;
- in [5], la pagina URL non è cambiata. La pagina visualizzata è la pagina 1.
7.4.2. L'azione [/ajax-06]
Il codice dell'azione [/ajax-06] è il seguente:
@RequestMapping(value = "/ajax-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax06() {
return "vue-06";
}
- righe 1-4: l'azione [/ajax-06] si limita a restituire la vista [vue-06.xml];
7.4.3. La vista [vue-06.xml]
La vista [vue-06.xml] è la seguente:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-06</title>
<link rel="stylesheet" href="/css/ajax01.css" />
<script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/local6.js"></script>
</head>
<body>
<h3>Ajax - 06 - Navigation dans une Application à Page Unique</h3>
<div id="content" th:include="vue-07" />
</body>
</html>
- riga 8: la vista utilizza uno script [local6.js];
- riga 12: si include la vista [vue-07.xml] nell'area con ID [content] della vista [vue-06.xml];
7.4.4. La vista [vue-07.xml]
La vista [vue-07.xml] è la seguente:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h4>Page 1</h4>
<p>
<a href="javascript:gotoPage(2)">Page 2</a>
</p>
</body>
</html>
7.4.5. La funzione jS [gotoPage]
Il collegamento [Page 2] della vista [vue-07.xml] utilizza la funzione jS [gotoPage] definita nel seguente file [local6.js]:
// dati globali
var content;
function gotoPage(num) {
// si effettua manualmente una chiamata Ajax
$.ajax({
url : '/ajax-07',
type : 'POST',
data : 'num=' + num,
dataType : 'html',
beforeSend : function() {
},
success : function(data) {
content.html(data)
},
complete : function() {
},
error : function(jqXHR) {
// errore di sistema
content.html(jqXHR.responseText);
}
})
}
// durante il caricamento del documento
$(document).ready(function() {
// si recuperano i riferimenti dei vari componenti della pagina
content = $("#content");
});
- riga 28: al caricamento della pagina, si memorizza l'area con ID [content] e la si trasforma in una variabile globale (riga 2);
- riga 4: la funzione [gotoPage] riceve come parametro il numero della pagina (1 o 2) da visualizzare nella vista corrente;
- riga 7: l'URL è il destinatario dell'POST;
- riga 8: la funzione URL della riga 7 viene richiamata tramite una POST;
- riga 9: la stringa inviata. Si tratta di un parametro denominato [num]. Il suo valore è il numero di pagina (riga 4) da visualizzare nella vista corrente;
- riga 10: il server restituirà HTML, ovvero il codice della pagina da visualizzare;
- righe 13-15: in caso di esito positivo (stato HTTP pari a 200), il valore HTML inviato dal server viene inserito nel campo ID [content];
- righe 18-20: in caso di errore (stato HTTP pari a 500), il HTML inviato dal server viene inserito nel campo ID [content];
7.4.6. L'azione [/ajax-07]
Il codice dell’azione [/ajax-07] è il seguente:
@RequestMapping(value = "/ajax-07", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax07(int num) {
// num: numero di pagina
switch (num) {
case 1:
return "vue-07";
case 2:
return "vue-08";
default:
return "vue-07";
}
}
- riga 2: si recupera il parametro inviato denominato [num]. Si ricorda che il parametro della riga 2 deve riportare il nome del parametro inviato, in questo caso [num]. [num] è un numero di pagina o di vista;
- righe 5-6: nel caso in cui [num==1], si restituisce la vista [vue-07.xml];
- righe 7-8: nel caso in cui [num==2], si restituisce la vista [vue-08.xml];
- righe 9-10: negli altri casi (normalmente impossibili), viene restituita la vista [vue-07.xml];
7.4.7. La vista [vue-08.xml]
La vista [vue-08.xml] costituisce la pagina n. 2 dell'applicazione:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h4>Page 2</h4>
<p>
<a href="javascript:gotoPage(1)">Page 1</a>
</p>
</body>
</html>
7.5. Incorporare più flussi HTML in una risposta jSON
7.5.1. Introduzione
Consideriamo la seguente applicazione:
![]() |
La pagina [1] presenta quattro aree:
- [Zone 1, Zone 3] sono aree che compaiono/scompaiono quando si fa clic sul pulsante [Rafraîchir]. Si conta il numero di volte in cui ciascuna di queste due aree [2] appare. L'area [Zone 1] utilizza la lingua francese, mentre l'area [Zone 3] utilizza la lingua inglese;
- la zona [Zone 2] è sempre presente;
- l'area [Saisies] è sempre presente;
Il link [Valider] visualizza la pagina successiva [3]:
![]() |
- il link [Retour à la page 1] riporta la pagina n. 1 allo stato in cui si trovava [4];
L'applicazione è a pagina singola. La prima pagina viene richiesta al server dal browser. Le pagine successive vengono recuperate dal server tramite chiamate Ajax.
7.5.2. L'azione [/ajax-09]
![]() |
L'azione [/ajax-09] è la seguente:
@RequestMapping(value = "/ajax-09", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax09() {
return "vue-09";
}
Si limita a visualizzare la vista [vue-09.xml].
7.5.3. Le viste XML
![]() |
La vista [vue-09.xml] è la pagina principale dell’applicazione:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-09</title>
<link rel="stylesheet" href="/css/ajax01.css" />
<script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/json3.js"></script>
<script type="text/javascript" src="/js/local9.js"></script>
</head>
<body>
<h3>Ajax - 09 - Navigation dans une Application à Page Unique</h3>
<h3>avec des flux HTML embarqués dans des chaînes jSON</h3>
<hr />
<div id="content" th:include="vue-09-page1" />
<img id="loading" src="/images/loading.gif" />
<div id="erreur" style="background-color:lightgrey"></div>
</body>
</html>
- riga 9: il file JS utilizzato nell'applicazione;
- riga 15: il contenuto della pagina principale;
- riga 16: un'immagine animata di attesa:
- riga 17: area per la visualizzazione di eventuali errori;
La vista [vue-09-page1.xml] è la pagina 1 dell'applicazione:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h2>Page 1</h2>
<!-- area 1 -->
<fieldset id="zone1" style="background-color:pink">
<legend>Zone 1</legend>
<span id="zone1-content" th:text="xx">xx</span>
</fieldset>
<!-- zona 2 -->
<fieldset id="zone2" style="background-color:lightgreen">
<legend>Zone 2</legend>
<span>Ce texte reste toujours présent</span>
</fieldset>
<!-- zona 3 -->
<fieldset id="zone3" style="background-color:yellow">
<legend>Zone 3</legend>
<span id="zone3-content" th:text="zz">zz</span>
</fieldset>
<br />
<p>
<button onclick="javascript:postForm()">Rafraîchir</button>
</p>
<hr />
<div id="saisies" th:include="vue-09-saisies">
</div>
</body>
</html>
- righe 6-9: l'area [Zone 1]. Il suo contenuto viene inserito nel componente [id="zone1-content"];
- righe 11-14: l’area [Zone 2], che rimane invariata;
- righe 16-19: il campo [Zone 3]. Il suo contenuto viene inserito nel componente [id="zone3-content"];
- riga 22: la funzione JS che invia il modulo;
- riga 25: inclusione dell'area di immissione dati;
Si noti che la pagina 1 non presenta il tag [form]. Tutto verrà gestito tramite JavaScript.
La vista [vue-09-saisies.xml] è la seguente:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<div id="saisies">
<h4>Saisies :</h4>
<p>
Chaîne de caractères :
<input type="text" id="text1" size="30" th:value="${value1}" />
</p>
<p>
Nombre entier :
<input type="text" id="text2" size="10" th:value="${value2}" />
</p>
<p>
<a href="javascript:valider()">Valider</a>
</p>
</div>
</html>
- righe 5-8: immissione di una stringa di caratteri;
- righe 13-16: immissione di un numero intero;
- riga 14: la funzione JS che invia i valori inseriti;
Ancora una volta, si noti che l’area di immissione non presenta il tag [form].
In totale, la pagina n. 1 presenta due funzionalità:
- [Rafraîchir]: che aggiorna le aree 1 e 3. Questa azione viene elaborata dal server che restituisce in modo casuale:
- l’area 1 con il suo contatore di accessi e nulla per l’area 3,
- la zona 3 con il suo contatore di accessi e nulla per la zona 1,
- entrambe le zone con i rispettivi contatori di accesso;
- [Valider]: che visualizza la pagina 2 con i valori inseriti oppure un messaggio di errore se i dati inseriti non sono validi;
Ci concentreremo innanzitutto sul pulsante [Rafraîchir].
7.5.4. Il codice JS per la gestione del pulsante [Rafraîchir]
![]() |
Il codice del file [local9.js] è il seguente:
// variabili globali
var content;
var loading;
var erreur;
// al caricamento del documento
$(document).ready(function() {
// si recuperano i riferimenti dei diversi componenti della pagina
loading = $("#loading");
loading.hide();
erreur = $("#erreur");
erreur.hide();
content = $("#content");
});
- righe 9-13: quando viene caricata la pagina master, vengono memorizzati i riferimenti ai tre componenti identificati da [loading, erreur, content];
- righe 2-4: i riferimenti di questi tre componenti vengono memorizzati in variabili globali. Rimangono fissi perché le tre aree interessate sono sempre presenti nella pagina visualizzata, indipendentemente dal momento. Poiché rimangono fisse, possono essere calcolate in [$(document).ready] e condivise con le altre funzioni del file JS;
La funzione [postForm] gestisce il clic sul pulsante [Rafraîchir]:
function postForm() {
console.log("postForm");
// si effettua manualmente una richiesta Ajax
$.ajax({
url : '/ajax-10',
headers : {
'Accept: 'application/json'
},
type : 'POST',
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
- righe 4-15: la chiamata Ajax al server;
- riga 5: è l’azione [ajax-10] che elaborerà il POST;
- righe 6-8: la risposta sarà jSON. Il client JS indica che accetta i documenti jSON;
- riga 9: l'azione [ajax-10] viene richiamata con un'operazione POST;
- riga 10: si riceverà da jSON;
- riga 11: la funzione eseguita prima della chiamata Ajax;
- riga 12: la funzione eseguita alla ricezione della risposta del server, quando questa ha esito positivo [200 OK];
- riga 13: la funzione eseguita alla ricezione della risposta del server, quando questa non va a buon fine [500 Internal server error, ...];
- riga 14: la funzione eseguita dopo aver ricevuto la risposta;
La funzione [onBegin] è la seguente:
// prima della richiesta Ajax
function onBegin() {
console.log("onBegin");
// immagine di caricamento
loading.show();
}
Si limita ad avviare l’immagine animata che indica l’attesa del risultato dal server.
7.5.5. L'azione [/ajax-10]
![]() |
L'azione [/ajax-10] è la seguente:
// la sessione
@Autowired
private SessionModel1 session;
// il motore Thymeleaf / Spring
@Autowired
private SpringTemplateEngine engine;
@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
@ResponseBody()
public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
...
}
- riga 3: si inserisce la sessione. Questa ha il tipo [SessionModel1] seguente:
![]() |
package istia.st.springmvc.models;
import java.io.Serializable;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel1 implements Serializable {
private static final long serialVersionUID = 1L;
// due contatori
private int cpt1 = 0;
private int cpt3 = 0;
// le tre aree
private String zone1 = "xx";
private String zone3 = "zz";
private String saisies;
private boolean zone1Active = true;
private boolean zone3Active = true;
// getter e setter
...
}
La sessione [SessionModel1] memorizza i seguenti elementi:
- riga 15: il numero di volte in cui [cpt1] viene visualizzato il campo [Zone 1];
- riga 16: il numero di volte [cpt3] in cui viene visualizzata l'area [Zone 3];
- righe 18-20: i flussi HTML delle aree [Zone 1], [Zone 3] e [Saisies]. Ciò è necessario nella sequenza [Page 1] --> [Page 2] --> [Page 1]. Quando si passa da [Page 2] a [Page 1], è necessario ripristinare [Page 1] e quindi i suoi tre campi;
- righe 21-22: due valori booleani che indicano se i campi [Zone 1] e [Zone 3] sono visualizzati (visibili);
L'altro elemento inserito nel controller [AjaxController] è il seguente:
// il motore Thymeleaf / Spring
@Autowired
private SpringTemplateEngine engine;
Il bean di tipo [SpringTemplateEngine] è definito nel file di configurazione [Config]:
![]() |
È definito come segue:
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(true);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
@Bean
SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
- righe 2-10: conosciamo il bean di tipo [SpringResourceTemplateResolver] che ci permette di definire alcune caratteristiche delle viste;
- righe 13-17: il bean di tipo [SpringTemplateEngine] ci permette di definire il "motore" delle viste, ovvero la classe incaricata di generare le risposte [Thymeleaf] ai client. [Thymeleaf] dispone di un “motore” predefinito e di un altro quando viene utilizzato in un ambiente [Spring]. È quest’ultimo che utilizziamo in questo caso;
La firma dell’azione [/ajax-10] è la seguente:
@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
@ResponseBody()
public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
...
}
- riga 1: l’azione [/ajax-10] accetta solo un POST;
- riga 2: l'azione [/ajax-10] restituisce essa stessa la risposta al cliente. Questa verrà trasformata automaticamente in jSON;
- riga 3: la risposta è del tipo [JsonResult10] come segue:
![]() |
package istia.st.springmvc.models;
public class JsonResult10 {
// dati
private String content;
private String zone1;
private String zone3;
private String erreur;
private String saisies;
private boolean zone1Active;
private boolean zone3Active;
public JsonResult10() {
}
// getter e setter
...
}
- riga 6: il contenuto HTML dell’area identificata da [content];
- riga 7: il contenuto HTML dell’area [Zone 1];
- riga 8: il contenuto HTML dell'area [Zone 3];
- riga 9: il contenuto HTML dell'area [Erreur];
- riga 10: il contenuto HTML dell'area [Saisies];
- riga 11: valore booleano che indica se l'area [Zone 1] deve essere visualizzata;
- riga 12: valore booleano che indica se l'area [Zone 3] deve essere visualizzata;
Il codice dell'azione [/ajax-10] è il seguente:
@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
@ResponseBody()
public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
// contesto Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// risposta
JsonResult10 result = new JsonResult10();
// sessione
session.setZone1(null);
session.setZone3(null);
session.setZone1Active(false);
session.setZone3Active(false);
// si restituisce una risposta casuale
int cas = new Random().nextInt(3);
switch (cas) {
case 0:
// zona 1 attiva
setZone1(thymeleafContext, result);
return result;
case 1:
// zona 3 attiva
setZone3(thymeleafContext, result);
return result;
case 2:
// zone 1 e 3 attive
setZone1(thymeleafContext, result);
setZone3(thymeleafContext, result);
return result;
}
return null;
}
- riga 5: recuperiamo il contesto [Thymeleaf]. Vedremo in seguito a cosa ci servirà;
- riga 7: per il momento creiamo una risposta vuota;
- righe 9-12: inseriamo in [null] i due campi contenuti nella sessione e specifichiamo che non devono essere visualizzati. Questi due campi verranno generati a breve, ma è possibile che ne venga generato solo uno;
- righe 14-29: vengono generati entrambi i campi;
- righe 17-19: viene generato solo il campo [Zone 1];
- righe 21-23: viene generato solo il campo [Zone 3];
- righe 25-28: vengono generate entrambe le zone [Zone 1] e [Zone 3];
Il flusso HTML del campo [Zone 1] viene generato con il seguente metodo:
private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
// zona 1 attiva
// flusso HTML
int cpt1 = session.getCpt1() + 1;
thymeleafContext.setVariable("cpt1", cpt1);
thymeleafContext.setLocale(new Locale("fr", "FR"));
String zone1 = engine.process("vue-09-zone1", thymeleafContext);
result.setZone1(zone1);
result.setZone1Active(true);
// sessione
session.setCpt1(cpt1);
session.setZone1(zone1);
session.setZone1Active(true);
}
- riga 1: i parametri sono:
- il contesto [Thymeleaf] di tipo [WebContext],
- la risposta al cliente in fase di costruzione di tipo [JsonResult10];
- riga 3: si incrementa il contatore [cpt1] della sessione che conta il numero di volte in cui viene visualizzato il campo [Zone 1];
- riga 4: il contesto [Thymeleaf] di tipo [WebContext] si comporta in modo simile al modello [Model] di Spring MVC. Per aggiungere un elemento al modello, si utilizza [WebContext.setVariable]. In questo caso, si inserisce quindi il contatore [cpt1] nel modello [Thymeleaf]. Ciò consentirà di valutare l’espressione Thymeleaf [${cpt1}]
- riga 5: il contesto [Thymeleaf] ha un’impostazione locale. Ciò gli consente di valutare espressioni del tipo [#{clé_msg}]. In questo caso, si associa il contesto Thymeleaf a un’impostazione locale francese;
- riga 6: questa è l’istruzione più interessante. Il motore Thymeleaf elaborerà la vista [vue-09-zone1.xml] con il modello e la locale appena calcolati e, invece di inviare il flusso risultante HTML al client, lo restituirà come stringa di caratteri;
- righe 7-9: il flusso HTML dell’area [Zone 1] appena calcolato viene memorizzato nella sessione e nel risultato che verrà inviato al client. Inoltre, si specifica che l’area [Zone 1] deve essere visualizzata;
- righe 11-13: si memorizzano nella sessione le informazioni relative all’area [Zone 1] per poterla rigenerare;
La riga 7 gestisce la vista [vue-09-zone1.xml] seguente:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<span th:text="#{message.zone}"></span>
<span th:text="${cpt1}"></span>
</html>
- riga 3: l'espressione [#{message.zone}] verrà valutata tramite la variabile locale;
- riga 4: l'espressione [${cpt1}] verrà valutata tramite il modello Thymeleaf;
Il messaggio chiave [message.zone] è definito nei file di messaggi [messages_fr.properties] e [messages_en.properties]:
![]() |
[messages_fr.properties]
message.zone=Nombre d'accès :
[messages_en.properties]
message.zone=Number of hits:
Il flusso HTML dell'area [Zone 3] viene generato con un metodo analogo:
private void setZone3(WebContext thymeleafContext, JsonResult10 result) {
// zona 3 attiva
// flusso HTML
int cpt3 = session.getCpt3() + 1;
thymeleafContext.setVariable("cpt3", cpt3);
thymeleafContext.setLocale(new Locale("en", "US"));
String zone3 = engine.process("vue-09-zone3", thymeleafContext);
result.setZone3(zone3);
result.setZone3Active(true);
// sessione
session.setCpt3(cpt3);
session.setZone3(zone3);
session.setZone3Active(true);
}
- riga 6: la lingua dell'area [Zone 3] è l'inglese;
7.5.6. Elaborazione della risposta dell'azione [/ajax-10]
Torniamo al codice JS di [local9.js] che elaborerà la risposta del server:
// al ricevimento della risposta del server
// in caso di esito positivo
function onSuccess(data) {
console.log("onSuccess");
// contenuto
if (data.content) {
content.html(data.content);
}
// zona 1
if (data.zone1Active) {
$("#zone1").show();
if (data.zone1) {
$("#zone1-content").html(data.zone1);
}
} else {
$("#zone1").hide();
}
// zona 3 attiva?
if (data.zone3Active) {
$("#zone3").show();
if (data.zone3) {
$("#zone3-content").html(data.zone3);
}
} else {
$("#zone3").hide();
}
// dati inseriti?
if (data.saisies) {
$("#saisies").html(data.saisies);
}
// errore?
if (data.erreur) {
erreur.text(data.erreur);
erreur.show();
} else {
erreur.hide();
}
}
Ricordiamo la struttura Java della risposta ricevuta alla riga 3 nella variabile [data]:
public class JsonResult10 {
// dati
private String content;
private String zone1;
private String zone3;
private String erreur;
private String saisies;
private boolean zone1Active;
private boolean zone3Active;
}
- righe 6-8: se [data.content!=null], allora si inizializza il campo [id=content] con tale valore. Questo campo rappresenta [Page 1] o [Page 2] nella sua interezza. Nella presente dimostrazione, abbiamo [data.content==null] e quindi l’area [id=content] non verrà modificata e continuerà a visualizzare [Page 1];
- righe 10-17: visualizzazione di [Zone 1] se [data.zone1Active==true]. Se inoltre [data.zone1!=null], allora il contenuto di [Zone 1] viene modificato, altrimenti rimane invariato;
- righe 19-26: lo stesso vale per [Zone 3];
- righe 28-30: se si ha [data.saisies!=null], allora l’area [Saisies] viene rigenerata. Nella dimostrazione in questione, si ha [data.saisies==null] e quindi l'area [Saisies] rimane invariata;
- righe 32-37: ragionamento analogo per l'area [Erreur] con le seguenti precisazioni:
- riga 33: [data.erreur] sarà un messaggio di errore in formato testo;
- riga 36: se [data.erreur==null], allora il campo [Erreur] viene nascosto. Infatti, potrebbe essere stato visualizzato durante la richiesta precedente;
In caso di errore lato server (HTTP, stato del tipo 500 Internal server error), viene eseguita la seguente funzione:
// al ricevimento della risposta dal server
// in caso di errore
function onError(jqXHR) {
console.log("onError");
// errore di sistema
erreur.text(jqXHR.responseText);
erreur.show();
}
Per visualizzare un errore di questo tipo, modifichiamo la funzione [postForm] come segue:
function postForm() {
console.log("postForm");
// si recuperano i riferimenti dalla pagina corrente
...
// si effettua una chiamata Ajax manualmente
$.ajax({
url : '/ajax-10x',
...
})
}
- riga 7: inseriamo un codice URL che non esiste;
Ecco i risultati quando si fa clic sul pulsante [Rafraîchir]:
![]() |
È interessante notare che anche l’errore è stato inviato sotto forma di stringa jSON.
Il metodo eseguito dopo aver ricevuto la risposta dal server è il seguente:
// dopo [onSuccess, onError]
function onComplete() {
console.log("onComplete");
// immagine di caricamento
loading.hide();
}
Ci si limita a nascondere l’immagine animata di attesa.
7.5.7. Visualizzazione della pagina [Page 2]
Il codice HTML del link [Valider] è il seguente:
<a href="javascript:valider()">Valider</a>
La funzione JS [valider] è la seguente:
// convalida dei valori inseriti
function valider() {
// valore inviato
var post = JSON3.stringify({
"value1" : $("#text1").val().trim(),
"value2" : $("#text2").val().trim()
});
// si effettua manualmente una chiamata Ajax
$.ajax({
url : '/ajax-11A',
headers : {
'Accept: 'application/json',
'Content-Type' : 'application/json'
},
type : 'POST',
data : post,
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
- righe 4-7: abbiamo due valori v1 e v2 da inviare: quelli dei componenti di immissione identificati da [#text1] e [#text2]. Faremo qualcosa di nuovo. Invieremo questi due valori sotto forma di stringa jSON {"value1":v1,"value2":v2};
- riga 10: i valori inviati saranno indirizzati all’azione [ajax-11A];
- riga 12: poiché sappiamo che riceveremo una risposta jSON, indichiamo che possiamo riceverne una da jSON;
- riga 13: si indica al server che gli verrà inviato il valore inviato tramite POST sotto forma di stringa jSON;
- righe 15-16: si crea un POST dal valore da inviare;
- riga 17: si riceverà un jSON;
7.5.8. L'azione [ajax-11A]
L'azione [ajax-11A] che elabora la stringa jSON inviata è la seguente:
@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
@ResponseBody
public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
...
}
- riga 1: con ["application/json"] si indica che l’azione attende un documento in formato jSON. Questo documento è il valore inviato dal cliente;
- riga 3: il valore inviato verrà recuperato nel seguente oggetto [PostAjax11A post]:
![]() |
package istia.st.springmvc.models;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Range;
public class PostAjax11A {
// dati
@Size(min = 4, max = 6)
@NotNull
private String value1;
@Range(min = 10, max = 14)
@NotNull
private Integer value2;
// getter e setter
...
}
- la struttura dell’oggetto [PostAjax11A] deve rispecchiare la struttura dell’oggetto inviato {"value1":v1,"value2":v2}. Sono quindi necessari i campi [value1] (riga 13) e [value2] (riga 16);
- sono stati applicati vincoli di integrità su entrambi i campi;
Torniamo al codice dell'azione [ajax-11A]:
@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
@ResponseBody
public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
// contesto Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// risposta
JsonResult10 result = new JsonResult10();
// post valido?
if (bindingResult.hasErrors()) {
// viene restituita la pagina 1 con un errore
result.setZone1Active(session.isZone1Active());
result.setZone3Active(session.isZone3Active());
result.setErreur(getErreursForModel(bindingResult));
return result;
}
...
}
- riga 3: l'annotazione [@RequestBody] indica il documento inviato dal cliente. Si tratta del valore inviato in jSON da quest'ultimo. Questo verrà quindi utilizzato per costruire l'oggetto [PostAjax11A];
- riga 3: l'annotazione [@Valid] impone la convalida del valore inviato;
- riga 9: se la convalida fallisce:
- riga 13: viene restituito un messaggio di errore;
- righe 11-12: i campi 1 e 3 vengono riportati allo stato in cui si trovavano (visualizzati o meno);
Il calcolo del messaggio di errore viene effettuato nel modo seguente:
private String getErreursForModel(BindingResult result) {
StringBuffer buffer = new StringBuffer();
for (FieldError error : result.getFieldErrors()) {
StringBuffer bufferCodes = new StringBuffer("(");
for (String code : error.getCodes()) {
bufferCodes.append(String.format("%s ", code));
}
bufferCodes.append(")");
buffer.append(String.format("[%s:%s:%s:%s]", error.getField(), error.getRejectedValue(), bufferCodes,
error.getDefaultMessage()));
}
return buffer.toString();
}
Si tratta di una funzione che abbiamo già incontrato.
L'azione [ajax-11A] prosegue come segue:
@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
@ResponseBody
public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
// contesto Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// risposta
JsonResult10 result = new JsonResult10();
// post valido?
if (bindingResult.hasErrors()) {
...
}
// si memorizza il campo di immissione
thymeleafContext.setVariable("value1", post.getValue1());
thymeleafContext.setVariable("value2", post.getValue2());
session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
// si invia la pagina 2
result.setContent(engine.process("vue-09-page2", thymeleafContext));
return result;
}
- righe 13-14: i valori inviati vengono inseriti nel contesto Thymeleaf;
- riga 15: con questo contesto, si calcola la vista [vue-09-saisies] e la si inserisce nella sessione per poterla rigenerare in seguito;
- riga 17: la pagina 2 viene inserita nel risultato che verrà inviato al client;
La vista [vue-09-page2.xml] è la seguente:
![]() |
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h2>Page 2</h2>
<p>
<h4>Valeurs saisies :</h4>
<p>
Chaîne de caractères :
<span th:text="${value1}"></span>
</p>
<p>
Nombre entier :
<span th:text="${value2}"></span>
</p>
<a href="javascript:retourPage1()">Retour à la page 1</a>
</p>
</body>
</html>
- nelle righe 9 e 13, vengono visualizzati i valori [value1, value2] che l’azione [/ajax-11A] ha inserito nel contesto Thymeleaf;
7.5.9. Elaborazione della risposta dell’azione [/ajax-11A]
Sul lato client, la risposta dell'azione [/ajax-10] viene elaborata dalla funzione [onSuccess]:
function onSuccess(data) {
console.log("onSuccess");
// contenuto
if (data.content) {
content.html(data.content);
}
// campo 1
if (data.zone1Active) {
$("#zone1").show();
if (data.zone1) {
$("#zone1-content").html(data.zone1);
}
} else {
$("#zone1").hide();
}
// campo 3 attivo?
if (data.zone3Active) {
$("#zone3").show();
if (data.zone3) {
$("#zone3-content").html(data.zone3);
}
} else {
$("#zone3").hide();
}
// dati inseriti?
if (data.saisies) {
$("#saisies").html(data.saisies);
}
// errore?
if (data.erreur) {
erreur.text(data.erreur);
erreur.show();
} else {
erreur.hide();
}
}
Abbiamo già commentato questo codice. Consideriamo i due casi: risposta con o senza errore:
Con errore
In questo caso, l’azione [/ajax-11A] ha inviato una risposta jSON del tipo {"zona1":null, "zona3":null,"dati inseriti":null,"errore":errore,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}. Se si segue il codice sopra riportato, si nota che:
- il campo [content] non cambia. Conteneva la pagina n. 1;
- viene visualizzata l’area [Erreur];
- le zone [Zone 1], [Zone 3], [Saisies] vengono lasciate così come erano;
Senza errori
In questo caso, l'azione [/ajax-11A] ha inviato una risposta jSON del tipo {"zona1":null, "zona3":null,"immissioni":null,"errore":null,"zone1Active":false,"zone3Active":false,"content":content}. Seguendo il codice sopra riportato, si nota che:
- viene visualizzata l’area [content]. Contiene la pagina n. 2;
Ecco tre esempi di esecuzione:
Un caso con errore di convalida:
![]() | ![]() |
Un caso con errore di POST:
![]() | ![]() |
Questo tipo di errore è diverso. Poiché Spring non è riuscito a convertire la stringa jSON nel tipo [PostAjax11A], ha restituito una risposta HTTP con [status=400]. L'azione [ajax-11A] non è stata eseguita;
Un caso senza errori:
![]() | ![]() |
7.5.10. Torna alla pagina n. 1
Il link [Retour vers la page 1] nella pagina n. 2 è il seguente:
<a href="javascript:retourPage1()">Retour à la page 1</a>
Il metodo JS [retourPage1] è il seguente:
// torna alla pagina 1
function retourPage1() {
// si effettua una chiamata Ajax manualmente
$.ajax({
url : '/ajax-11B',
headers : {
'Accept: 'application/json',
},
type : 'POST',
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
Esso genera un POST, senza valore inserito, verso l’azione [/ajax-11B].
7.5.11. L'azione [/ajax-11B]
L'azione [/ajax-11B] è la seguente:
@RequestMapping(value = "/ajax-11B", method = RequestMethod.POST)
@ResponseBody
public JsonResult10 ajax11B(HttpServletRequest request, HttpServletResponse response) {
// contesto Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// risposta
JsonResult10 result = new JsonResult10();
// ripristiniamo la pagina 1 al suo stato originale
result.setContent(engine.process("vue-09-page1", thymeleafContext));
result.setSaisies(session.getSaisies());
result.setZone1(session.getZone1());
result.setZone3(session.getZone3());
result.setZone1Active(session.isZone1Active());
result.setZone3Active(session.isZone3Active());
return result;
}
L'azione deve rigenerare la pagina n. 1 con le sue tre aree [Zone1, Zone3, Erreur]:
- riga 9: la pagina n. 1 viene inserita nel risultato;
- riga 10: l'area di immissione dati viene inserita nel risultato;
- riga 11: l'area [Zone 1] viene inserita nel risultato;
- riga 12: l’area [Zone 3] viene inserita nel risultato;
- righe 13-14: si inseriscono nel risultato lo stato dei campi [Zone 1] e [Zone 3];
7.5.12. Elaborazione della risposta dell'azione [/ajax-11B]
La risposta dell'azione [/ajax-11B] viene elaborata dalla funzione [onSuccess]:
function onSuccess(data) {
console.log("onSuccess");
// contenuto
if (data.content) {
content.html(data.content);
}
// area 1
if (data.zone1Active) {
$("#zone1").show();
if (data.zone1) {
$("#zone1-content").html(data.zone1);
}
} else {
$("#zone1").hide();
}
// zona 3 attiva?
if (data.zone3Active) {
$("#zone3").show();
if (data.zone3) {
$("#zone3-content").html(data.zone3);
}
} else {
$("#zone3").hide();
}
// dati inseriti?
if (data.saisies) {
$("#saisies").html(data.saisies);
}
// errore?
if (data.erreur) {
erreur.text(data.erreur);
erreur.show();
} else {
erreur.hide();
}
}
L'azione [/ajax-11B] ha inviato una risposta jSON del tipo {"zona1":zona1, "zona3":zona3,"dati inseriti":dati inseriti,"errore":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content}. Se si esamina il codice sopra riportato, si nota che:
- il campo [content] è stato modificato. Conteneva la pagina n. 2. D'ora in poi conterrà la pagina n. 1;
- l’area [Erreur] è nascosta;
- le zone [Zone 1], [Zone 3], [Saisies] vengono visualizzate così come erano;
7.6. Gestione della sessione lato client
7.6.1. Introduzione
Nel paragrafo precedente abbiamo gestito una sessione con la seguente struttura:
public class SessionModel1 implements Serializable {
// due contatori
private int cpt1 = 0;
private int cpt3 = 0;
// le tre zone
private String zone1 = "xx";
private String zone3 = "zz";
private String saisies;
private boolean zone1Active = true;
private boolean zone3Active = true;
...
}
Quando gli utenti sono molto numerosi, la memoria occupata dalle sessioni di tutti questi utenti può rappresentare un problema. La regola è quindi quella di ridurne al minimo le dimensioni. Il modello APU (Applicazione a pagina singola) consente di gestire la sessione lato client e di avere un server web senza sessioni. Infatti, la pagina singola viene caricata inizialmente dal browser. Insieme ad essa viene caricato il file JavaScript che la accompagna. Poiché non vi è alcun ricaricamento della pagina, questo file JS rimarrà permanentemente all’interno del browser così come è stato caricato inizialmente. È quindi possibile utilizzare le sue variabili globali per memorizzarvi informazioni sulle diverse azioni dell’utente. È proprio ciò che vedremo ora. Non solo gestiremo la sessione lato client, ma ripenseremo l’applicazione JS in modo da sollecitare il meno possibile il server.
7.6.2. L'azione [/ajax-12]
![]() |
L’azione [/ajax-12] è la seguente:
@RequestMapping(value = "/ajax-12", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax12() {
return "vue-12";
}
La vista [vue-12.xml] è la seguente:
![]() |
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-12</title>
<link rel="stylesheet" href="/css/ajax01.css" />
<script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/json3.js"></script>
<script type="text/javascript" src="/js/local12.js"></script>
</head>
<body>
<h3>Ajax - 12 - Navigation dans une Application à Page Unique</h3>
<h3>avec des flux HTML embarqués dans une chaîne jSON</h3>
<h3>et une session gérée par le client JS</h3>
<hr />
<div id="content" th:include="vue-09-page1" />
<img id="loading" src="/images/loading.gif" />
<div id="erreur" style="background-color:lightgrey"></div>
</body>
</html>
- questa vista è identica alla vista [vue-09], con la sola differenza dello script JS utilizzato alla riga 9;
La vista visualizzata è la seguente:
![]() |
7.6.3. Il codice JS per la gestione del pulsante [Rafraîchir]
![]() |
Il codice del file [local12.js] è il seguente:
// variabili globali
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
"cpt1" : 0,
"cpt3" : 0
};
// al caricamento del documento
$(document).ready(function() {
// si recuperano i riferimenti dei diversi componenti della pagina
loading = $("#loading");
loading.hide();
erreur = $("#erreur");
erreur.hide();
content = $("#content");
});
- righe 17-21: quando viene caricata la pagina master, si memorizzano i riferimenti dei tre componenti identificati da [loading, erreur, content] nelle variabili globali delle righe 2-4;
- righe 5-6: per memorizzare le due pagine;
- righe 7-8: per memorizzare i due valori inviati tramite il link [Valider];
- riga 9: la sessione. Memorizza lato client i valori dei contatori [cpt1, cpt3];
La funzione [postForm] gestisce il clic sul pulsante [Rafraîchir]:
function postForm() {
console.log("postForm");
// si imposta la sessione
var post = JSON3.stringify(session);
// si effettua manualmente una chiamata Ajax
$.ajax({
url : '/ajax-13',
headers : {
'Accept: 'application/json',
'Content-Type' : 'application/json'
},
type : 'POST',
data : post,
dataType : 'json',
beforeSend : onBegin,
success : function(data) {
...
},
error : onError,
complete : onComplete
})
}
Le differenze rispetto alla versione precedente sono le seguenti:
- la funzione URL alla riga 7 è diversa;
- riga 4: viene inviato un valore, mentre in precedenza non ne veniva inviato alcuno. Questo valore è la stringa jSON della sessione. Il principio è il seguente:
- il client invia la sessione al server,
- il server la modifica e la rinvia,
- il client memorizza la nuova sessione;
- riga 10: si invia un documento in formato jSON (valore inviato);
- riga 13: si ha qualcosa da spedire;
- righe 15-20: le funzioni [beforeSend, error, complete] sono quelle della versione precedente. Cambia solo la funzione [success] (righe 16-18);
7.6.4. L'azione [/ajax-13]
![]() |
L'azione [/ajax-13] è la seguente:
@RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody()
public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request, HttpServletResponse response) {
...
}
- riga 3: il parametro [@RequestBody SessionModel2 session2] recupera la sessione inviata dal client. Questa ha il seguente tipo [SessionModel2]:
![]() |
package istia.st.springmvc.models;
import java.io.Serializable;
public class SessionModel2 implements Serializable {
private static final long serialVersionUID = 1L;
// due contatori
private int cpt1 = 0;
private int cpt3 = 0;
// getter e setter
...
}
La sessione [SessionModel2] memorizza i seguenti elementi:
- riga 9: il numero di volte in cui [cpt1] viene visualizzato il campo [Zone 1];
- riga 10: il numero di volte [cpt3] in cui viene visualizzata l'area [Zone 3];
Continuiamo l’analisi del codice dell’azione [/ajax-13]:
@RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody()
public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request, HttpServletResponse response) {
...
}
- riga 3, il tipo [JsonResult13] della risposta è il seguente:
![]() |
package istia.st.springmvc.models;
public class JsonResult13 {
// dati
private String page2;
private String zone1;
private String zone3;
private String erreur;
private String value1;
private Integer value2;
// sessione
private SessionModel2 session;
// getter e setter
...
}
- riga 14: la sessione. Il server la rinvia al client affinché venga memorizzata;
- riga 6: il contenuto HTML della pagina n. 2;
- riga 7: il contenuto HTML dell'area [Zone 1];
- riga 8: il contenuto HTML dell'area [Zone 3];
- riga 9: l'eventuale messaggio di errore;
- righe 10-11: due informazioni calcolate dal server e visualizzate dalla pagina n. 2;
Continuiamo l’analisi del codice dell’azione [/ajax-13]:
@RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody()
public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,
HttpServletResponse response) {
// contesto Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// risposta
JsonResult13 result = new JsonResult13();
result.setSession(session2);
// si restituisce una risposta casuale
int cas = new Random().nextInt(3);
switch (cas) {
case 0:
// zona 1 attiva
setZone1B(thymeleafContext, result);
return result;
case 1:
// zona 3 attiva
setZone3B(thymeleafContext, result);
return result;
case 2:
// zone 1 e 3 attive
setZone1B(thymeleafContext, result);
setZone3B(thymeleafContext, result);
return result;
}
return null;
}
- riga 9: la sessione viene inserita nel risultato dell’azione;
Il metodo [setZone1B] che attiva l'area [Zone 1] è il seguente:
private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
// si recupera la sessione
SessionModel2 session = result.getSession();
// zona 1 attiva
// flusso HTML
int cpt1 = session.getCpt1() + 1;
thymeleafContext.setVariable("cpt1", cpt1);
thymeleafContext.setLocale(new Locale("fr", "FR"));
String zone1 = engine.process("vue-09-zone1", thymeleafContext);
result.setZone1(zone1);
// sessione
session.setCpt1(cpt1);
}
- riga 3: si recupera la sessione. Questa verrà modificata alla riga 12 con il nuovo contatore [cpt1]. Si ricorda che questa sessione verrà rinviata al cliente;
- riga 10: il nuovo campo [Zone 1];
Il metodo [setZone3B] che attiva il campo [Zone 3] è analogo:
private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
// si recupera la sessione
SessionModel2 session = result.getSession();
// zona 3 attiva
// flusso HTML
int cpt3 = session.getCpt3() + 1;
thymeleafContext.setVariable("cpt3", cpt3);
thymeleafContext.setLocale(new Locale("en", "US"));
String zone3 = engine.process("vue-09-zone3", thymeleafContext);
result.setZone3(zone3);
// sessione
session.setCpt3(cpt3);
}
7.6.5. Elaborazione della risposta dell’azione [/ajax-13]
Sul lato client, la risposta jSON dell'azione [/ajax-13] viene elaborata dalla seguente funzione [onSuccess]:
function postForm() {
console.log("postForm");
// si invia la sessione
var post = JSON3.stringify(session);
// si effettua manualmente una richiesta Ajax
$.ajax({
...
success : function(data) {
// si memorizza la sessione
session = data.session;
// si aggiornano i due campi
if (data.zone1) {
$("#zone1-content").html(data.zone1);
$("#zone1").show();
} else {
$("#zone1").hide();
}
if (data.zone3) {
$("#zone3").show();
$("#zone3-content").html(data.zone3);
} else {
$("#zone3").hide();
}
},
...
})
}
- righe 12-17: se il server ha inserito qualcosa nel campo [zone1] della risposta, allora è necessario rigenerare il campo [Zone 1] e visualizzarlo, altrimenti deve essere nascosto;
- righe 18-23: stesso ragionamento per l'area [Zone 3];
7.6.6. Visualizzazione della pagina [Page 2]
Il codice HTML del link [Valider] è il seguente:
<a href="javascript:valider()">Valider</a>
La funzione JS [valider] è la seguente:
// convalida dei valori inseriti
function valider() {
// si salva la pagina 1
page1 = content.html();
// si salvano i valori inseriti
value1 = $("#text1").val().trim();
value2 = $("#text2").val().trim();
// valore inviato
var post = JSON3.stringify({
"value1" : value1,
"value2" : value2,
"pageRequired" : page2 ? false : true
});
// si effettua manualmente una chiamata Ajax
$.ajax({
url : '/ajax-14',
headers : {
'Accept: 'application/json',
'Content-Type' : 'application/json'
},
type : 'POST',
data : post,
dataType : 'json',
beforeSend : onBegin,
success : function(data) {
...
},
error : onError,
complete : onComplete
})
}
- eseguiremo un POST che normalmente ci porterà alla pagina n. 2;
- riga 4: si memorizza la pagina n. 1 per poterci tornare in seguito;
- righe 6-7: l'operazione precedente non memorizza i valori inseriti, ma solo il codice HTML della pagina. Pertanto, ora si memorizzano i due valori inseriti nel modulo;
- righe 9-13: i due valori inseriti vengono inseriti in una stringa jSON. È questa che verrà inviata;
- riga 12: un parametro per indicare al server se è necessaria la pagina n. 2. Procederemo in questo modo. Richiederemo la pagina n. 2 una prima volta, quindi la memorizzeremo nella variabile JS [page2]. Successivamente, non la richiederemo più. Utilizzeremo la pagina presente nella cache. Riga 2: [pageRequired] è uguale a [true] se la variabile [page2] è vuota, altrimenti è uguale a [false];
- si noti che la sessione non viene inviata. Infatti, essa memorizza dei contatori che l’azione [/ajax-14] della riga 20 non modifica;
7.6.7. L'azione [/ajax-14]
L'azione [/ajax-14] è la seguente:
@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
@ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
...
}
- riga 3: la risposta è sempre di tipo [JsonResult13];
- riga 3: il valore inviato è incapsulato nel seguente tipo [PostAjax14]:
package istia.st.springmvc.models;
public class PostAjax14 extends PostAjax11A {
// pagina 2
private boolean pageRequired;
// getter e setter
...
}
- riga 3: la classe [PostAjax14] estende la classe [PostAjax11A] della versione precedente. Ha quindi una struttura [value1, value2, pageRequired];
L'azione [/ajax-14] prosegue come segue:
@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
@ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
// contesto Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// risposta
JsonResult13 result = new JsonResult13();
// post valido?
if (bindingResult.hasErrors()) {
// viene restituito un errore
result.setErreur(getErreursForModel(bindingResult));
return result;
}
// si invia la pagina 2
result.setValue1(post.getValue1());
result.setValue2(post.getValue2());
// pagina richiesta?
if (post.isPageRequired()) {
result.setPage2(engine.process("vue-12-page2", thymeleafContext));
}
return result;
}
- righe 9-13: se i valori inviati [value1, value2] non sono validi, viene restituito un messaggio di errore;
- righe 15-16: normalmente, il server dovrebbe eseguire un calcolo con i valori inviati. In questo caso, si limita a rinviarli per indicare che li ha ricevuti correttamente;
- righe 18-20: la pagina n. 2 viene restituita solo se è stata richiesta dal client. Riga 19, la vista [vue-12-page2] è nuova:
![]() |
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h2>Page 2</h2>
<p>
<h4>Valeurs saisies :</h4>
<p>
Chaîne de caractères :
<span id="value1"></span>
</p>
<p>
Nombre entier :
<span id="value2"></span>
</p>
<a href="javascript:retourPage1()">Retour à la page 1</a>
</p>
</body>
</html>
- il codice XML non contiene più valori valutati da Thymeleaf come avveniva in precedenza;
- sono state identificate le aree in cui inserire i valori restituiti dal server [value1, value2]. Riga 9, [id='value1'] indica il punto in cui inserire [value1]. Riga 13, lo stesso vale per [value2];
7.6.8. Elaborazione della risposta dell'azione [/ajax-14]
La risposta dell'azione [/ajax-14] viene elaborata dalla seguente funzione [success]:
// convalida dei valori inseriti
function valider() {
...
// si effettua manualmente una chiamata Ajax
$.ajax({
...
success : function(data) {
// errore?
if (data.erreur) {
// visualizzazione dell'errore
erreur.html(data.erreur);
erreur.show();
} else {
// nessun errore
erreur.hide();
// pagina 2
if (page2) {
// si utilizza la pagina memorizzata nella cache
content.html(page2);
} else {
// si memorizza la pagina 2
page2 = data.page2;
// viene visualizzata
content.html(data.page2);
}
// la si aggiorna con le informazioni dal server
$("#value1").text(data.value1);
$("#value2").text(data.value2);
}
},
...
})
}
- righe 9-13: se il server ha restituito un errore, lo si visualizza;
- righe 14-29: nel caso in cui non si sia verificato alcun errore, si visualizza la pagina n. 2;
- riga 17: si verifica se la pagina n. 2 è già memorizzata nella variabile [page2];
- riga 19: in questo caso, si utilizza la variabile [page2] per visualizzare la pagina n. 2;
- riga 24: in caso contrario, si utilizza il campo [data.page2] fornito dal server;
- riga 22: ci si assicura di memorizzare la pagina n. 2 per non doverla richiedere nuovamente in seguito;
- righe 27-28: nella pagina n. 2, si visualizzano le due informazioni [value1, value2] inviate dal server;
7.6.9. Torna alla pagina n. 1
Il link [Retour vers la page 1] nella pagina n. 2 è il seguente:
<a href="javascript:retourPage1()">Retour à la page 1</a>
Il metodo JS [retourPage1] è il seguente:
// ritorno alla pagina 1
function retourPage1() {
// si rigenera la pagina 1
content.html(page1);
// si rigenerano i dati inseriti
$("#text1").val(value1);
$("#text2").val(value2);
}
- si tratta di un'azione JS senza interazione con il server poiché la pagina n. 1 è stata memorizzata localmente nella variabile [page1];
- riga 4: si rigenera la pagina n. 1;
- righe 6-7: era stata memorizzata solo la parte HTML della pagina n. 1. Non i dati inseriti. È quindi necessario rigenerare questi ultimi;
7.6.10. Conclusione
Sfruttando le potenzialità del modello APU, siamo riusciti a semplificare il server web, che ora è stateless (assenza di sessione) e meno sollecitato:
- abbiamo eliminato l’interazione con il server nella funzione JS ([retourPage1]);
- il server genera la pagina n. 2 una sola volta;
7.7. Strutturazione del codice JavaScript a livelli
7.7.1. Introduzione
Il codice JavaScript dell’applicazione precedente sta iniziando a diventare complesso. È giunto il momento di strutturarlo a livelli. L’applicazione rimarrà la stessa di prima. Non interverremo sul server, se non per definire una nuova pagina iniziale. Riorganizzeremo il codice JS.
La nuova architettura sarà la seguente:
![]() |
7.7.2. La pagina iniziale
L’azione che avvia l’applicazione è la seguente: [/ajax-16]:
@RequestMapping(value = "/ajax-16", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax16() {
return "vue-16";
}
Visualizza la seguente vista [vue-16.xml]:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-12</title>
<link rel="stylesheet" href="/css/ajax01.css" />
<script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/json3.js"></script>
<script type="text/javascript" src="/js/local16-dao.js"></script>
<script type="text/javascript" src="/js/local16-ui.js"></script>
</head>
<body>
<h3>Ajax - 16 - Navigation dans une Application à Page Unique</h3>
<h3>Structuration du code JS</h3>
<hr />
<div id="content" th:include="vue-09-page1" />
<img id="loading" src="/images/loading.gif" />
<div id="erreur" style="background-color:lightgrey"></div>
</body>
</html>
- righe 9-10: il codice JS è stato inserito in due file diversi:
- [local-ui] implementa il livello [présentation],
- [local-dao] implementa il livello [DAO];
![]() |
7.7.3. Implementazione del livello [DAO]
![]() |
7.7.4. Interfaccia
Il livello [DAO] in [local-dao.js] presenterà la seguente interfaccia al livello [présentation]:
| per aggiornare la pagina 1 con il pulsante [Rafraîchir] |
| per visualizzare la pagina 2 con il pulsante [Valider] |
JavaScript non prevede il concetto di interfaccia. Ho utilizzato questo termine semplicemente per indicare che il livello [présentation] si impegnava a interagire con il livello [DAO] esclusivamente tramite le due funzioni precedenti.
7.7.5. Implementazione dell’interfaccia
Lo scheletro dell’implementazione è il seguente:
var session = {
"cpt1" : 0,
"cpt3" : 0
};
// aggiornamento della pagina 1
function updatePage1(deferred, sendMeBack) {
...
}
// pagina 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}
Lo scopo del livello [DAO] è quello di nascondere al livello [présentation] i dettagli delle richieste HTTP inviate al server web. La sessione fa parte di questi dettagli. È quindi ora gestita dal livello [DAO].
7.7.5.1. La funzione [updatePage1]
La funzione [updatePage1] è la funzione chiamata dal livello [présentation] per aggiornare la pagina 1. Il suo codice è il seguente:
// aggiornamento pagina 1
function updatePage1(deferred, sendMeBack) {
// richiesta HTTP
executePost(deferred, sendMeBack, '/ajax-13', session);
}
- riga 1: la funzione [updatePage1] riceve due parametri:
- un oggetto di tipo [jQuery.Deferred]. Questo tipo di oggetto memorizza uno stato che può assumere tre valori ['pending', 'resolved', 'rejected']. Quando arriva nella funzione [updatePage1], si trova nello stato [pending];
- un oggetto JS da restituire nel livello [présentation];
Tutte le richieste HTTP vengono eseguite dalla seguente funzione [executePost]:
// richiesta HTTP
function executePost(deferred, sendMeBack, url, post) {
// si effettua manualmente una chiamata Ajax
$.ajax({
headers : {
'Accept: 'application/json',
'Content-Type: 'application/json'
},
url : url,
type : 'POST',
data : JSON3.stringify(post),
dataType : 'json',
success : function(data) {
// si memorizza la sessione
if (data.session) {
session = data.session;
}
// si restituisce il risultato
deferred.resolve({
"status" : 1,
"data" : data,
"sendMeBack" : sendMeBack
});
},
error : function(jqXHR) {
// si restituisce l'errore
deferred.resolve({
"status" : 2,
"data" : jqXHR.responseText,
"sendMeBack" : sendMeBack
});
}
});
}
- riga 1: la funzione [executePost] esegue una chiamata Ajax di tipo POST. Richiede quattro parametri:
- un oggetto di tipo [jQuery.Deferred] nello stato [pending];
- un oggetto JS da restituire nel livello [présentation];
- l'oggetto URL derivato da POST;
- il valore da inviare come oggetto JS;
- righe 5-8: la funzione invia il jSON (riga 7) e riceve il jSON (riga 6);
- riga 11: il valore da inviare viene trasformato in jSON;
- righe 13-24: la funzione eseguita in caso di esito positivo della chiamata Ajax;
- righe 19-23: se il server ha restituito una sessione, questa viene memorizzata;
- righe 13-18: trasferiscono l'oggetto [deferred] allo stato [resolved], passando inoltre un risultato con i seguenti campi:
- [status]: 1 in caso di esito positivo, 2 in caso di esito negativo,
- [data]: la risposta jSON del server,
- [sendMeBack]: il secondo parametro della funzione, ovvero un oggetto che il chiamante desidera recuperare;
- righe 17-31: la funzione eseguita in caso di fallimento della chiamata Ajax. Si procede come in precedenza con due differenze:
- [status] passa a 2 per segnalare un errore;
- [data] è ancora una volta la risposta jSON del server, ma ottenuta in modo diverso;
7.7.5.2. La funzione [getPage2]
La funzione [getPage2] è la seguente:
// pagina 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
// richiesta HTTP
executePost(deferred, sendMeBack, '/ajax-14', {
"value1" : value1,
"value2" : value2,
"pageRequired" : pageRequired,
});
}
- la funzione riceve i seguenti parametri:
- [deferred]: un oggetto di tipo [jQuery.Deferred] nello stato [pending],
- [sendMeBack]: un oggetto JS da restituire nel livello [présentation],
- [value1]: il primo inserimento nella pagina 1,
- [value2]: il secondo inserimento nella pagina 2,
- [pageRequired]: un valore booleano che indica al server se deve o meno inviare il flusso HTML della pagina n. 2;
- viene chiamata la funzione [executePost] per eseguire la richiesta HTTP necessaria;
7.7.6. Il livello [présentation]
![]() |
Il livello [présentation] è implementato dal file [local-ui.js]. Quest'ultimo riprende il codice del file [local12.js], rielaborato per utilizzare il precedente livello [DAO]. Sono state modificate solo due funzioni: [postForm] e [valider].
7.7.6.1. La funzione [postForm]
La funzione [postForm] è la seguente:
// aggiornamento Pagina 1
function postForm() {
// si aggiorna la pagina 1
var deferred = $.Deferred();
loading.show();
updatePage1(deferred, {
'mittente: "postForm",
'info: 10
});
// visualizzazione dei risultati
deferred.done(postFormDone);
}
- riga 4: si crea un oggetto [jQuery.Deferred]. Per impostazione predefinita, si trova nello stato [pending];
- riga 5: viene visualizzata l'immagine di attesa
- righe 6-9: viene eseguita la funzione [updatePage1]. Si passa un oggetto fittizio [sendMeBack], solo per mostrare a cosa può servire;
- riga 11: il parametro della funzione [deferred.done] è a sua volta una funzione. Si tratta della funzione da eseguire quando lo stato dell’oggetto [deferred] passa allo stato [resolved]. Abbiamo appena visto che la funzione DAO [executePost] trasferiva lo stato di questo oggetto a [resolved] al ricevimento della risposta dal server. Ciò significa che quando viene eseguita la funzione [postFormDone], la risposta del server è già stata ricevuta;
La funzione [postFormDone] è la seguente:
function postFormDone(result) {
// fine attesa
loading.hide();
// si recuperano i dati
var data = result.data
// per la demo
console.log(JSON3.stringify(result.sendMeBack));
// si analizza lo stato
switch (result.status) {
case 1:
// si aggiornano i due campi
if (data.zone1) {
$("#zone1-content").html(data.zone1);
$("#zone1").show();
} else {
$("#zone1").hide();
}
if (data.zone3) {
$("#zone3").show();
$("#zone3-content").html(data.zone3);
} else {
$("#zone3").hide();
}
break;
case 2:
// visualizzazione errore
erreur.html(data);
break;
}
}
- riga 1: il parametro [result] ricevuto è il parametro passato al metodo [deferred.resolve] nella funzione [executePost], ad esempio:
// restituzione del risultato
deferred.resolve({
"status" : 1,
"data" : data,
"sendMeBack" : sendMeBack
});
- riga 5: si recupera la risposta dal server;
- righe 10-24: il codice che nella versione precedente si trovava nella funzione [onSuccess] della funzione [postForm];
- righe 25-28: il codice che nella versione precedente si trovava nella funzione [onError] della funzione [postForm];
7.7.6.2. Il ruolo del parametro [sendMeBack]
A cosa serve il parametro [sendMeBack]? Esaminiamo il codice di chiamata della funzione [updatePage1]:
// aggiornamento Pagina 1
function postForm() {
// si aggiorna la pagina 1
var deferred = $.Deferred();
loading.show();
updatePage1(deferred, {
'mittente: "postForm",
'info: 10
});
// visualizzazione dei risultati
deferred.done(postFormDone);
}
e la firma della funzione [validerDone]:
function postFormDone(result) {
}
In che modo la funzione [postForm] può trasmettere informazioni alla funzione [postFormDone]? Quest’ultima ha un solo parametro, [result]. Quest’ultimo viene creato dalla funzione [executePost] del livello [DAO]. Per trasmettere informazioni alla funzione [postFormDone], la funzione [postForm] deve prima trasmetterle alla funzione [updatePage1]. Questo è il ruolo del parametro [sendMeBack]. Si utilizza nel modo seguente:
function postFormDone(result) {
// fine attesa
loading.hide();
// si recuperano i dati
var data = result.data
// per la demo
console.log(JSON3.stringify(result.sendMeBack));
// si analizza lo stato
switch (result.status) {
...
- riga 7, la funzione [postFormDone] ha recuperato il parametro [sendMeBack] inizialmente trasmesso alla funzione DAO [updatePage1] dalla funzione [postForm];
7.7.7. La funzione [valider]
La funzione [valider] è la seguente:
// convalida dei valori inseriti
function valider() {
// si salva la pagina 1
page1 = content.html();
// si salvano i valori inseriti
value1 = $("#text1").val().trim();
value2 = $("#text2").val().trim();
// nessun errore
erreur.hide();
// viene richiesta la pagina 2
var deferred = $.Deferred();
loading.show();
getPage2(deferred, {
'mittente: 'conferma',
'info' : 20
}, value1, value2, page2 ? false : true);
// visualizzazione dei risultati
deferred.done(validerDone);
}
e la funzione [validerDone] (riga 18) è la seguente:
function validerDone(result) {
// fine attesa
loading.hide();
// si recuperano i dati
var data = result.data
// per la demo
console.log(JSON3.stringify(result.sendMeBack));
// si analizza lo stato
switch (result.status) {
case 1:
// errore?
if (data.erreur) {
// visualizzazione errore
erreur.html(data.erreur);
erreur.show();
} else {
// nessun errore
erreur.hide();
// pagina 2
if (page2) {
// si utilizza la pagina in cache
content.html(page2);
} else {
// si memorizza la pagina 2
page2 = data.page2;
// viene visualizzata
content.html(data.page2);
}
// la si aggiorna con le informazioni dal server
$("#value1").text(data.value1);
$("#value2").text(data.value2);
}
break;
case 2:
// visualizzazione dell'errore
erreur.html(data);
erreur.show();
break;
}
}
- riga 5: si recupera la risposta dal server;
- righe 10-32: qui si trova il codice che nella versione precedente era contenuto nella funzione [onSuccess] della funzione [valider];
- righe 34-38: il codice che nella versione precedente si trovava nella funzione [onError] della funzione [valider];
7.7.8. Test
L'applicazione continua a funzionare come prima e nella console di Chrome si possono vedere i parametri [sendMeBack] delle funzioni [postForm] e [valider]:
![]() |
7.8. Conclusion
Torniamo allo schema generale di un’applicazione Spring MVC:
![]() |
Grazie al JavaScript incorporato nelle pagine HTML ed eseguito nel browser e grazie al modello APU, è possibile trasferire il codice sul browser e ottenere la seguente architettura:
![]() |
- si ottiene un'architettura client [2] / server [1] in cui client e server comunicano tramite jSON;
- in [1], il livello web Spring MVC fornisce viste, frammenti di vista e dati in jSON;
- in [2]: il codice JavaScript incorporato nella vista caricata all'avvio dell'applicazione può essere strutturato in livelli:
- il livello [présentation] gestisce le interazioni con l'utente,
- il livello [DAO] gestisce l'accesso ai dati tramite il server web [1],
- il livello [métier] potrebbe non esistere oppure riprendere alcune delle funzionalità non riservate del livello [métier] del server al fine di alleggerirne il carico;
- il client [2] può memorizzare alcune viste nella cache, sempre al fine di alleggerire il carico del server. Gestisce la sessione;













































































