L'acronimo più in voga tra gli sviluppatori di applicazioni web è senza dubbio AJAX (Asynchronous Javascript And Xml), attraverso il quale è possibile realizzare Rich Internet Application.
AJAX risolve i problemi legati alla necessità, che un client ha, di comunicare con il server per le operazioni che l'utente richiede. Come fa? Delegando all'interprete Javascript del browser questo compito.
Nell'articolo non andremo a vedere AJAX, per il quale rimandiamo alla guida, ma la sua implementazione insieme alle pagine JSP.
Esempio pratico
L'esempio che mostreremo si occupa di gestire il caricamento dinamico di informazioni all'interno di un elemento ben specificato della pagina. Leggendo la guida citata nell'introduzione, infatti, noterete che la funzione che più di ogni altra è sinonimo di AJAX è quella che consente la manipolazione di elementi in una pagina HTML.
Ciò può essere realizzato mediante la proprietà innerHtml degli elementi HTML (un div, generalmente): sostituire il contenuto, o aggiungerlo, prelevandolo da una pagina esterna è quello che andremo a fare.
In questo modo, attraverso la comparsa (a discesa, ad esempio) di informazioni aggiuntive, precedute da elementi di attesa tipici di queste applicazioni (barre di scorrimento, clessidre, ecc), sembrerà tutto molto più immediato, più fluido, più accattivante.
Vogliamo realizzare una web application che permette di visualizzare dei generici oggetti in forma di lista, visualizzare il dettaglio di un oggetto, mantenendo la stessa lista sempre visibile, ed effettuare la funzione di voto (tipica delle applicazioni user generated content) ancora una volta rimanendo nella stessa pagina. Per rendere l'esempio più vivace immaginiamo di dover gestire uno store di film, nel quale gli utenti visualizzano le informazioni e danno un voto numerico.
Le funzioni di logica applicativa sono quindi tre:
- Mostrare tutti i film;
- Mostrare il dettaglio di un film;
- Votare un film.
Per la gestione del controllo utilizzeremo una servlet.
La servlet si poggia ad una classe, DataLayer, a cui sarà delegata la logica di persistenza (attraverso JDBC o EJB). I dati sono virtualizzati dall'interfaccia Item, che espone i metodi che andremo ad utilizzare nelle viste listItem.jsp e detail.jsp.
Prima di discutere della parte AJAX, vediamo il contenuto della servlet, giusto per capire quali sono i dati che le due viste si aspettano di trovare. In pratica, i tre metodi (richiamati dal metodo service in base al parametro op) ricalcano i tre metodi di logica necessari all'applicazione.
Listato 1. Servelt che si interfaccia con AJAX
public class ActionServlet extends HttpServlet {
DataLayer dl;
private void doGetList(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Collection toRet=dl.getList();
RequestDispatcher rd=this.getServletContext().getRequestDispatcher("/listItem.jsp");
req.setAttribute("list",toRet);
rd.forward(req,resp);
}
private void doGetItem(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
int id=Integer.parseInt(req.getParameter("iid"));
Item toRet=dl.getItemById(id);
RequestDispatcher rd=this.getServletContext().getRequestDispatcher("/detail.jsp");
req.setAttribute("list",toRet);
rd.forward(req,resp);
}
private void doVoteItem(HttpServletRequest req, HttpServletResponse resp) throws IOException {
int id=Integer.parseInt(req.getParameter("iid"));
double vote=Double.parseDouble(req.getParameter("vote"));
Item toRet=dl.getItemById(id);
toRet.vote(vote);
resp.getWriter().println("Il tuo voto per "+toRet.getTitle()+" è stato registrato!");
}
}
Ora passiamo alla parte AJAX e per le funzioni di base riutilizzeremo le librerie della guida citata nell'introduzione.
La vista listItem.jsp, viene richiamata dalla servlet con il metodo forward che si occupa di passarle la lista di Item come attributo di pagina. Questa pagina recupererà la lista e mostrerà i film (immagine e titolo), predisponendo per ognuno di essi un elemento di tipo div con identificativo univoco DIVIDXX dentro il quale al momento della scelta dell'utente, attraverso AJAX, inseriremo il contenuto recuperato dalla vista detail.jsp.
Listato 2. Pagina JSP con immagine e titolo
<!—- listItem.jsp -->
<%@page import="java.util.*,it.html.ajaxjsp.Item"%>
<html>
<head>
<title>AJAX e JSP</title>
<link rel="stylesheet" href="style.css">
<script language="javascript" src="./js/ajax.js"></script>
</head>
<body>
...//
<div class="content">
<h3>Lista degli oggetti:</h3>
<%
//Iteriamo gli elementi caricati dalla servlet
Collection list=(Collection)request.getAttribute("list");
Iterator it=list.iterator();
while (it.hasNext()){
Item x=(Item)it.next();
%>
<!-- Mostriamo immagine e titolo del film. -->
<div class="list">
<span><img src="./img/<%=x.getPicture()%>"></span>
<span><a href="javascript:openBox(<%=x.getId()%>)"><strong><%=x.getTitle()%></strong></a></span>
</div>
<!-- Quest'elemento è un elemento vuoto e nascosto.
La funzione javascript, openBox, lo renderà visibile, e
caricherà qui dentro il contenuto specifico attraverso
la proprietà innerHtml dell'elemento. -->
<div id="DIVID<%=x.getId()%>" class="description"></div>
<%
}
%>
</div>
</body>
</html>
L'elemento contenitore (uno per ogni oggetto) è un elemento nascosto e vuoto (la classe "description" all'interno del css ha come attributo display:none
). Al momento del click sullo specifico film, per cui l'utente vuole visualizzarne il dettaglio, verrà lanciata la funzione javascript openBox()
valorizzata con l'id dell'elemento da riempire e visualizzare.
È openBox()
al suo interno ad effettuare una richiesta AJAX verso il server (attraverso 'actionservlet?op=getItem&iid='+id
). La servlet, a fronte di questa richiesta, risponderà con il contenuto della vista detail.jsp valorizzata con il film specifico.
Quindi, questo contenuto, verrà innestato dentro l'elemento DIVIDid che sempre la stessa funzione si occuperà di rendere visibile (stile display:block
).
In maniera del tutto asincrona (sulla base del click dell'utente), si effettua una richiesta verso il server per recuperare delle informazioni che vanno a modificare uno degli elementi della pagina attualmente visualizzata.
Listato 3. Richiesta verso il server per recuperare le informazioni (Guarda il codice completo)
//ajax.js
...//
function openBox(id){
elemento="DIVID"+id show(elemento);
setLoading(elemento);
caricaTesto(elemento,'actionservlet?op=getItem&iid='+id+'');
}
function show(elemento){
elemento = prendiElementoDaId(elemento);
elemento.style.display='block';
}
function setLoading(elemento) {
elemento = prendiElementoDaId(elemento);
elemento.innerHTML = "<div class="waiting"><img align="bottom" src="./img/waiting.gif"/>Loading...</div>";
}
function caricaTesto(elemento,nomeFile) {
var
ajax = assegnaXMLHttpRequest(),
elemento = prendiElementoDaId(elemento),
usaLink = true;
if(ajax) {
usaLink = false;
ajax.open("get", nomeFile, true);
ajax.setRequestHeader("connection", "close");
ajax.onreadystatechange = function() {
if(ajax.readyState === readyState.COMPLETATO) {
if(statusText[ajax.status] === "OK"){
elemento.innerHTML = ajax.responseText;
} else {
elemento.innerHTML = "Impossibile effettuare l'operazione richiesta.<br />";
elemento.innerHTML += "Errore riscontrato: " + statusText[ajax.status];
}
}
}
ajax.send(null);
}
};
...//
La funzione openBox()
valorizza il nome dell'elemento, lo mostra, attraverso il metodo show()
, lo valorizza con un'immagine di attesa (utile quando il risultato dell'elaborazione reale impiega molto tempo) ed infine recupera il dettaglio dalla vista, scrivendo il risultato sull'elemento.
Della funzione caricaTesto()
ho riportato i passaggi fondamentali, quelli che ci mostrano come, a partire da un url, sia piuttosto semplice recuperare delle informazioni ed includerle in un elemento della pagina utilizzando la proprietà innerHTML.
Nel caso non si utilizza l'approccio Model 2 (MVC), si può richiamare direttamente la pagina detail.jsp come url.
ajax = assegnaXMLHttpRequest()
: il metodo recupera l'oggetto javascriptXMLHttpRequest
, che è differente in base al tipo e alla versione del browser;elemento = prendiElementoDaId(elemento)
: utilizzando il metodogetElementById
, consente di recuperare l'elemento specificato, il cui id deve essere univoco all'interno della pagina html;ajax.open("get", nomeFile, true)
: effettua la comunicazione asincrona (flag "true") verso l'url "nomefile" in modalità "get". Nel caso di questa modalità i parametri dovranno essere passati direttamente nella url.elemento.innerHTML = ajax.responseText
: scrive all'interno dell'elemento "elemento" la risposta del server (testuale) alla precedente interrogazione. Si tenga presente che se il server produce codice html, questo verrà interpretato con le sue specifiche regole (quindi fare attenzione a come i tag sono innestati). è presente anche la proprietàresponseXML
che permette di navigare la risposta come documento XML.
Quindi è importante definire la vista detail.jsp, che non sarà più una pagina web, ma una pagina "contenuto" del div DIVIDXX.
Listato 4. Pagina che andrà all'interno del div
<!—- detail.jsp -->
<%@page import="it.html.ajaxjsp.Item"%>
<%Item x=(Item)request.getAttribute("list");%>
<%=x.getDescription()%>
Media: <%=x.getRatingAverage()%> (<%=x.getRatingNumber()%> voti)
<input id="ID<%=x.getId()%>" type="text">
<input type="button" value="Vota!" onclick="javascript:vote('ID<%=x.getId()%>',this,<%=x.getId()%>)">
La pagina ha lo scopo di rappresentare quello che dovrà essere contenuto nel div, cioè la descrizione e le altre informazioni, quindi, la pagina non ha elementi html se non quelli necessari. Importante, ai fini di una corretta visualizzazione sul browser è che siano rispettate le regole di innesto dei tag (apertura e chiusura).
Per ogni film avevamo inoltre previsto una funzione che permettesse all'utente di dare un giudizio. Per fare ciò predisponiamo un modulo di inserimento e un bottone che richiama il metodo javascript vote()
passando come parametri l'id del modulo di inserimento e l'id del film.
Il metodo vote()
, in maniera asincrona, andrà ad effettuare la richiesta sul server, sempre attraverso i metodi AJAX, richiamando questa volta l'url definito per votare un film (actionservlet?op=voteItem&iid=...
). In questo caso prenderemo la stringa restituita dal server e la inseriremo in un alert javascript, mostrando all'utente il risultato della sua operazione.
Listato 5. Pagina per dare la possibilità all'utente di votare
//ajax.js
//La funzione si occupa di registrare il voto per lo specifico
itemfunction vote(voto,bott,id){
//Recupero lo specifico elemento che contiene il voto
inpVoto=document.getElementById(voto);
if (vote!=""){
//Controllo sul range di valori
if (inpVoto.value>0 && inpVoto.value<=10){
//OK: disabilitiamo i tasti, in modo da prevenire altre azioni utente
bott.disabled=true;
inpVoto.disabled=true;
//Chiamiamo la funzione per il setting del voto al volo
alertTesto('actionservlet?op=voteItem&iid='+id+'&vote='+inpVoto.value+'');
} else {
//Valori fuori range
alert("Il voto deve essere compreso tra 1 e 10.");
}
}
// la funzione richiama nomeFile e mostra il risultato in un alertfunction
alertTesto(nomeFile) {
Var ajax = assegnaXMLHttpRequest()
...//
if(statusText[ajax.status] === "OK"){
// operazione avvenuta con successo
alert(ajax.responseText);
} else {
...//
};
}
}
La funzione alertTesto()
non è per nulla differente dalla funzione caricaTesto()
se non nel fatto che la prima contiene all'interno di un alert la risposta del server.
Succederà quindi che, quando l'utente clicca, se il voto inserito è corretto, verrà richiamata la servlet, la cui risposta verrà mostrata in un alert javascript (senza essere mai usciti dalla pagina).
Vediamo ora attraverso le immagini, il risultato del nostro esempio: all'ingresso dell'applicazione (scompattate il file zip e mettetelo sotto la webapps del vostro container), clicchiamo sul link "lista oggetti" che è collegato alla servlet, dal metodo di visualizzazione della lista.
L'utente decide di visualizzare il dettaglio del secondo film e clicca sul titolo:
La pagina cambia solo nell'elemento sotto il secondo film, dando l'effetto dell'apertura a discesa (sottolineato dai bordi che lo racchiudono), visualizzando il dettaglio del film.
Poi l'utente decide di visualizzare il dettaglio del primo film e di effettuare un voto per quest'ultimo:
La finestra ci dice che il voto è stato registrato. Siamo rimasti sempre all'interno della stessa pagina.