martedì 22 gennaio 2008

Capitolo 4 - Miglioriamo l’ AjaxCore

Introduzione



Utilizzando la prima versione del nostro AjaxCore si presentano alcuni piccoli problemi.
Uno degli aspetti piu’ complicati da gestire, sotto certe condizioni, e’ la sincronizzazione tra ricezione dei dati richiesti al server e l’event handler collegato che si deve occupare della distribuzione dei dati ricevuti nei vari contenitori o layer presenti nella nostra Graphical User Interface.
A seconda del tipo di operazione che vogliamo poter effettuare siamo costretti ad inserire una opzione specifica nell’event handler nelle funzioni loadHTMLDoc o loadHTMLPost.
Oppure dobbiamo ampliare il numero di funzioni disponibili come abbiamo fatto per la funzione getBeanResult e per il wrapper setValToDB , e le sottovarianti che da essa posso derivare.

In aggiunta a questo aspetto, quando abbiamo la necessita’ di effettuare piu’ di una operazione in cascata a partire dalla ricezione dei dati richiesti, dovremmo inserire delle chiamate indirette ad altre funzioni javascript (anonime oppure no) in modo statico, cioe’ senza avere la possibilita’, per ora, di gestirle di volta in volta a seconda dei casi.

Facciamo un esempio.

Abbiamo sulla nostra interfaccia grafica un form che mostra l’elenco dei dipendenti con nome, cognome, email ed altri dati collegati.

Supponiamo di voler aggiornare l’elenco dei dipendenti a fronte dell’ immissione di un nuovo record. Dopo che il dato viene inviato all’Application Server e memorizzato sul database, abbiamo la necessita’ di effettuare un refresh della tabella mostrata sul client con il nuovo record appena inserito.

Di certo dobbiamo gestire l’eccezione dovuta ad un errore nel salvataggio sul database del nuovo dato. Quindi il refresh della nostra tabella necessariamente deve seguire il salvataggio del dato sul server. Deve seguire strettamente quell’evento. E dobbiamo anche pero’ sapere che il salvataggio ha avuto esito positivo. E dobbiamo saperlo prima di effettuare il refresh.

Abbiamo percio’ il seguente diagramma temporale:


Il periodo che intercorre tra l’invio al server della richiesta di aggiornamento dei dati e’ non definito. Ovvero , trattandosi di una transizione asincrona, abbiamo il controllo solo dell’evento che viene scatenato quando riceviamo un messaggio da parte del server che ci avverte dell’avvenuto aggiornamento. Ma non sappiamo a priori il momento esatto in cui avverra’. Dobbiamo quindi avere a disposizione un meccanismo per poter lanciare la seconda eventuale richiesta in cascata alla ricezione del messaggio di esito positivo. Cioe’ se l’esito dell’aggiornamento e’ negativo non effettuiamo la seconda richiesta.

Un modo per far cio’ e’ personalizzare una chiamata ad un metodo all’interno dell’event handler loadHTMLPost tramite un parametro.
E inserire nel metodo un test sul messaggio ritornato dal server dopo la prima transazione.

Cio’ e’ difficilmente gestibile nell’ unico event handler statico oppure usando dei cloni di questo event handler. Ci occorre un event handler anonimo personalizzabile di volta in volta.

La libreria Net – il Metodo ContentLoader

Come per la libreria json.js ci viene in aiuto una libreria OpenSource. Vediamo il listato seguente:



Questa libreria implementa un Oggetto “net” che al suo interno ha il metodo ContentLoader() che si occupa dell’interfacciamento con il server remoto in modo asincrono.


  • Il primo parametro - url - referenzia l’url che vogliamo recuperare sull’ application server. Obbligatorio.

  • Il secondo parametro - onload - referenzia l’event handler che vogliamo attivare sull’evento onreadystatechange dell’oggetto XMLHttpRequest. Obbligatorio.

  • Il terzo - onerror - referenzia un event handler in caso di errore (error handler). E’ un parametro opzionale, in quanto in assenza del parametro viene referenziato un error handler di default.

  • Il quarto parametro - method - indica il metodo di trasmissione da applicare [GET – POST]. Il default e’ impostato su GET. Generalmente e’ preferito l’uso del metodo POST.

  • Il quinto parametro - contentType - ci aiuta a gestire l’interpretazione della querystring passata in abbinamento al method. Se vogliamo passare coppie di chiavi-valori come dati usando il metodo POST dobbiamo usare il contentType application/x-www-form-urlencoded. Di default e’ impostato su questa modalita’.

  • Gli ultimi due parametri riguardano la possibilita’ di fare interrogazioni usando il protocollo SOAP (per i web services) e la possibilita’ di gestire i privilegi su browsers della famiglia Mozilla. Noi non useremo questi parametri.

Javascript Prototypes

Questa libreria utilizza alcuni costrutti avanzati del linguaggio Javascript, tipo l’oggetto prototype e i metodi alternativi per definire una funzione. Diamo solo un breve accenno all’oggetto prototype. Come vediamo dal codice all’interno del metodo costruttore ContentLoader si istanziano attributi interni e metodi dell’oggetto, tra cui il metodo this.loadXMLDoc().

Pero’ nel costruttore si dichiara solo il metodo demandando successivamente la sua effettiva implementazione tramite il metodo prototype.

Quasto metodo permette di definire “a posteriori” un metodo di un oggetto, dopo che l’oggetto e’ stato costruito solo con le sue dichiarazioni.

In realta’ il metodo prototype fa molto di piu’. Esso permette di istanziare una proprieta’ o un metodo a posteriori a tutte le istanze di un oggetto.
Per chi non sia avvezzo alla programmazione ad oggetti, mentre una classe e’ la modellizzazione dell’oggetto con le sue proprieta’ e i suoi metodi, le istanze della classe sono gli oggetti veri e propri che si utilizzano all’interno del nostro codice.



Descrizione del nucleo del costruttore ContentLoader

In questo modo possiamo aggiungere tre metodi alle istanze del nostro oggetto. In realta’ si tratta di un metodo , di un event handler interno costruito su una funzione anonima e di un error handler , anch’esso interno al nostro oggetto.

Questa sezione di codice, interna al primo metodo si occupa del caricamento dell’oggetto XMLHttpRequest, restituendo un riferimento req se il caricamento va a buon fine.


Successivamente se il riferimento req e’ definito, cioe’ se if(this.req)==true, si effettua il passaggio dei parametri passati ai vari elementi dell’oggetto asincrono, l’apertura del collegamento con l’application server e il collegamento dell’ event handler di onreadystatechange all’eventhandler esterno passato col riferimento onload attraverso la chiamata loader.onReadyState.call(loader);

In realta’ quello che avviene e’ che il primo metodo attiva in modo indiretto l’event handler interno all’oggetto net tramite la funzione call(caller) .


Questo secondo metodo (l’handler) si occupa della gestione del codice associato all’evento onreadystatechange per attivare il metodo esterno in modo corretto solo quando la transazione ha segnalato il corretto codice di completamento avvenuto in modo positivo con il codice HTTP 200 (OK).


Se sul completamento della transazione non si ha codice 200 oppure 0, si richiama l’error handler. Se non ne e’ stato definito uno personalizzato viene chiamato quello di default, che visualizza un semplice alert con visualizzato il codice di errore HTTP e la relativa descrizione breve.

Il Lazy Function Definition Pattern e il problema del Warm-Up

Questo pattern, formalizzato da Peter Michaux nell’articolo sul sito http://peter.michaux.ca/article/3556 indica una soluzione efficiente per il problema del warm-up di un oggetto.
In sostanza:



var foo = function() {
var t = new Date();
foo = function(){
return t;
}
return foo(); // return t;
};

la prima volta che chiamiamo la funzione si istanzia l’oggetto Date( ), pero’ si sovrascrive la funzione stessa con un’altra che ritorna solo il reference all’oggetto appena istanziato.
Prima del termine della definizione della funzione esterna si ritorna la funzione sovrascritta, per restituire al chiamante l’oggetto istanziato.
In realta’ esiste una ulteriore ottimizzazione possibile: al posto di ritornare la chiamata alla funzione sovrascritta possiamo ritornare direttamente il reference interno all’oggetto, come facciamo all’interno della funzione sovrascritta:


return t;


in questo modo le volte successive che chiamiamo la funzione foo( ) in realta stiamo chiamando la funzione sovrascritta, ricevendo immediatamente il reference all’oggetto istanziato la prima volta.

Nel caso del nostro oggetto XMLHttpRequest non possiamo pero’ usare lo stesso oggetto per tutte le richieste, perche’ l’attributo this.req.responseText varia per ogni chiamata. Dobbiamo per forza istanziare un oggetto per ognuna di esse.

Aggiunta della gestione delle richieste Sincrone

A volte in casi molto particolari abbiamo la necessita’ di effettuare richieste verso il werver in modo assolutamente sincrono. Ovvero quando effettuiamo la richiesta al server vogliamo che il nostro Ajax Engine rimanga in attesa della risposta da parte del server in modo prioritario rispetto a qualunque altra operazione. Per far cio’ dobbiamo modificare leggermente la nostra libreria che finora suppone di dover effettuare sempre richieste asincrone, aggiungendo un parametro opzionale che indichi con un valore booleano “true” se la richiesta deve essere effettuata in modo sincrono.

Di seguito riporto la libreria cosi’ modificata evidenziando i punti su cui sono intervenuto.


var net=new Object();
net.READY_STATE_UNINITIALIZED=0;
net.READY_STATE_LOADING=1;
net.READY_STATE_LOADED=2;
net.READY_STATE_INTERACTIVE=3;
net.READY_STATE_COMPLETE=4;
var statusText = new Array();
statusText[100] = "Continue";
statusText[101] = "Switching Protocols";
statusText[200] = "OK";
statusText[201] = "Created";
statusText[202] = "Accepted";
statusText[203] = "Non-Authoritative Information";
statusText[204] = "No Content";
statusText[205] = "Reset Content";
statusText[206] = "Partial Content";
statusText[300] = "Multiple Choices";
statusText[301] = "Moved Permanently";
statusText[302] = "Found";
statusText[303] = "See Other";
statusText[304] = "Not Modified";
statusText[305] = "Use Proxy";
statusText[306] = "(unused, but reserved)";
statusText[307] = "Temporary Redirect";
statusText[400] = "Bad Request";
statusText[401] = "Unauthorized";
statusText[402] = "Payment Required";
statusText[403] = "Forbidden";
statusText[404] = "Not Found";
statusText[405] = "Method Not Allowed";
statusText[406] = "Not Acceptable";
statusText[407] = "Proxy Authentication Required";
statusText[408] = "Request Timeout";
statusText[409] = "Conflict";
statusText[410] = "Gone";
statusText[411] = "Length Required";
statusText[412] = "Precondition Failed";
statusText[413] = "Request Entity Too Large";
statusText[414] = "Request-URI Too Long";
statusText[415] = "Unsupported Media Type";
statusText[416] = "Requested Range Not Satisfiable";
statusText[417] = "Expectation Failed";
statusText[500] = "Internal Server Error";
statusText[501] = "Not Implemented";
statusText[502] = "Bad Gateway";
statusText[503] = "Service Unavailable";
statusText[504] = "Gateway Timeout";
statusText[505] = "HTTP Version Not Supported";
statusText[509] = "Bandwidth Limit Exceeded";
net.getXHR = function(){
var http;
try {
http = new XMLHttpRequest;
//alert("new http object");
net.getXHR = function() {
//alert("reload old http object");
return new XMLHttpRequest;
};
}
catch(e) {
//alert("ActiveX version");
var msxml = [
'MSXML2.XMLHTTP.3.0',
'MSXML2.XMLHTTP',
'Microsoft.XMLHTTP'
];
for (var i=0, len = msxml.length; i < len; ++i) {
try {
http = new ActiveXObject(msxml[i]);
net.getXHR = function() {
return new ActiveXObject(msxml[i]);
};
break;
}
catch(e) {}
}
}
//alert("deploy");
return http;
}
net.ContentLoader=function(url,onload,onerror,method,params,contentType,headers,secure,noasync){
this.req=null;
this.flag=null;
this.onload=onload;
this.onerror=(onerror) ? onerror : this.defaultError;
this.secure=secure;
this.loadXMLDoc(url,method,params,contentType,headers,noasync);
}
net.ContentLoader.prototype={
loadXMLDoc:function(url,method,params,contentType,headers,noasync){
if (!method){
method="GET";
}
if (!contentType && method=="POST"){
contentType='application/x-www-form-urlencoded';
}
this.req = net.getXHR();
if (this.req){
try{
try{
if (this.secure && netscape && netscape.security.PrivilegeManager.enablePrivilege)
{
netscape.security.PrivilegeManager.enablePrivilege('UniversalBrowserRead');
}
}catch (err){}
if(noasync){
this>.req.open(method,url,false);
}else{
this.req.open(method,url,true);
}
if (contentType){
this.req.setRequestHeader('Content-Type', contentType);
}
if (headers){
for (var h in headers){
this.req.setRequestHeader(h,headers[h]);
}
}
var loader=this;
this.req.onreadystatechange=function(){
loader.onReadyState.call(loader);
}
//this.req.setRequestHeader( 'If-Modified-Since', 'Thu, 06 Apr 2000 00:00:00 GMT' );
this.req.send(params);
}catch (err){
this.onerror.call(this);
}
}
},
onReadyState:function(){
var req=this.req;
var ready=req.readyState;
if (ready==net.READY_STATE_COMPLETE){
var httpStatus=req.status;
if (httpStatus==200 || httpStatus==0){
try{
if (this.secure && netscape && netscape.security.PrivilegeManager.enablePrivilege)
{
netscape.security.PrivilegeManager.enablePrivilege('UniversalBrowserRead');
}
}catch (err){}
this.onload.call(this);
}else{
this.onerror.call(this);
}
}
},
defaultError:function(){
alert("Critical Network Error!!! error fetching data!"
+"\n\nreadyState:"+this.req.readyState
+"\nstatus: "+statusText[this.req.status]
+"\nheaders: "+this.req.getAllResponseHeaders());
}
}

Riassunto

Grazie alla libreria Javascript appena analizzata abbiamo adesso a disposizione l’oggetto net e il metodo ContentLoader per gestire sia il caricamento dell’oggetto XMLHttpRequest sia l’utilizzo di un event handler collegato in modo assolutamente dinamico. Non dobbiamo cioe’ piu’ predefinire l’event handler all’interno del nostro AjaxCore. In piu’, come vderemo successivamente, abbiamo anche un semplice modo per effettuare chiamate multiple a cascata perfettamente sincronizzate tra di loro. In piu’ abbiamo anche a disposizione un metodo per personalizzare eventualmente un error handler. Abbiamo inoltre visto come ottimizzare il caricamento dell’oggetto XMLHttpRequest usando il Pattern LFD.

Nessun commento: