Quando pensiamo ad una applicazione web non possiamo non tener conto dei problemi legati allo scambio di informazioni tra client e server. Possiamo cercare di rendere il più snello possibile questo flusso di dati evitando ad esempio un numero eccessivo di postback.
È anche a partire da considerazioni come queste che si sono sviluppate librerie AJAX come ATLAS per ASP.NET
Con ASP.NET 1.x, quindi, per superare il problema derivante dai continui postback, si impiega l'oggetto Microsoft XMLHTTP ActiveX che serve ad inviare richieste ai metodi lato server, da funzioni Javascript lato client. In ASP.NET 2.0 questo processo è stato semplificato con l'introduzione del Client Callback Manager.
Il Client Callback Manager di ASP.NET 2.0 fornisce la possibilità di invocare una funzione o un metodo lato server di una pagina web senza effettuare l'aggiornamento del browser. Affinché funzioni il Client Callback Manager, il browser naturalmente deve supportare l'XMLHTTP.
Il Client Callback Manager può quindi essere usato per aggiornare singoli controlli o per la validazione o anche per inviare i dati di un form senza inviare tutta la pagina. Il callback avviene attraverso script Javascript che inviano e ricevono dati sotto forma di flussi XML.
Quindi un evento che si verifica sul client, gestito da uno script presente sul client, può inviare una richiesta di processamento al server in modo asincrono, senza causare un postback.
Il server possiede alcuni metodi che ricevono la richiesta del client, la processano ed inviano la risposta. Infine, lato client, viene ricevuta ed elaborata la risposta del server.
La comunicazione fra le due parti avviene attraverso una stringa che contiene sia il comando da eseguire, sia i dati da inviare o ricevere.
Implementare il Client Callback Manager
Per usufruire del Client Callback Manager dobbiamo implementare l'interfaccia ICallBackEventHandler
nella nostra pagina. Il che significa aggiungere l'interfaccia alla dichiarazione della classe.
Implementare l'interfaccia ICallbackEventHandler
public partial class _Default : System.Web.UI.Page, ICallbackEventHandler
A questo che dobbiamo realizzare concretamente i metodi dell'interfaccia:
- RaiseCallbackEvent
- GetCallbackResult
RaiseCallbackEvent()
è invocato quando il client invia una richiesta al server. Questo metodo si occupa di leggere ed interpretare la stringa inviata dal client per poi eseguirne il comando con i dati inviati.
GetCallbackResult()
è invocato dopo RaiseCallbackEvent()
e restituisce la stringa da inviare al client come risposta della richiesta effettua.
Abbiamo poi bisogno di una stringa, che possiamo chiamare 'callbackStr', da utilizzare lato client e che conterrà il codice Javascript per lo scambio dei dati con il server. Questa stringa deve essere dichiarata pubblica e valorizzata con il metodo GetCallbackEventReference della classe ClientScriptManager
.
La firma completa del metodo è la seguente:
GetCallbackEventReference(Control control, string argument, string clientCallback, string context, string errorCallback, Boolean async).
I parametri di GetCallbackEventReference sono i seguenti:
Control control
è il controllo che implementa ilRaiseCallbackEvent
. Se il metodo è implementato nella pagina, il riferimento saràthis
;string argument
è un valore che viene inviato alRaiseCallbackEvent
attraverso il parametroeventArgument
;string clientCallback
è il nome del gestore di eventi lato client che riceve il risultato inviato dal server;string context
è un valore che identifica chi ha iniziato il callback;string errorCallback
è il nome del gestore di errore sul client che riceve la risposta di errore dal server se questa si verifica;Boolean async
è un valore booleano che specifica se i callback vanno eseguiti in modo asincrono.
Il metodo GetCallbackEventReference
quindi serve a costruire il metodo Javascript lato client WebForm_DoCallback
che viene utilizzato dai gestori di eventi sul client per effettura le chiamate al server.
Il Client Callback Manager raccontato a parole sembra più complicato di quello che in effetti è. Con l'esempio ci chiariremo meglio le idee e avremo modo di apprezzare questa tecnica.
L'esempio
Realizziamo un esempio per comprendere meglio il funzionamento di questo meccanismo. Creiamo un piccolo form per la ricerca di una località attraverso filtri successivi. Facciamo selezionare all'utente prima una nazione, poi una regione e finalmente una città.
Le selezioni avvengono con delle liste a discesa (DropDownList
). Normalmente ad ogni selezione su un certo livello del filtro segue un postback col quale viene richiesta la lista delle opzioni per il livello successivo. Vediamo come evitare di dover ricaricare la pagina ogni volta ed ottenere lo stesso risultato.
Realizzazione con postback
Costruiamo una semplice tabella di database come quella mostrata in figura:
Il database di esempio rappresenta, in modo molto semplificato, le informazioni sugli appartamenti che una società immobiliare ha a disposizione per la locazione e che vuole fornire ai clienti, che si collegano al sito.
Vogliamo realizzare una pagina in cui l'utente possa scegliere la nazione fra quelle disponibili. In base a questa scelta avrà una lista di regioni disponibili ed in base alla regione scelta, avrà una lista di città. Scegliendo la città verrà visualizzata una tabellina con gli appartamenti a disposizione.
Realizzare una simile pagina con i controlli che ASP.NET 2.0 ci mette a disposizione è molto semplice. Potremmo utilizzare tre: una DropDownList
, un GridView
e quattro DataSource
:
Con una simile realizzazione però, ad ogni scelta avviene un postback, cioè tutta la pagina viene inviata al server che la processa e la riinvia al client.
Per verificare che avviene il postback possiamo mettere una write all'interno del Page_Load
oppure semplicemente notare che la barra di avanzamento del browser si mette in azione:
Oltre a questo, la sequenza di andata e ritorno dal server è lenta perché si porta dietro molte informazioni che, nel nostro caso, non sarebbero necessarie.
Nella prossima puntata vedremo come realizzare l'esempio con il Client Callback Manager.
Realizzazione con Client Callback Manager
Nella parte precedente dell'articolo abbiamo realizzato il nostro esempio usando la tecnica classica del postback ora realizziamo la stessa pagina facendo uso del Client Callback Manager.
Posizioniamo sulla pagina le tre DropDownList
che riempiremo dinamicamente. La tabellina invece verrà costruita sul client con i dati inviati dal server.
Listato 1. Le DropDownList sul codice sorgente della Default.aspx
...
<table>
<tr>
<td style="width: 136px"> Scegli la nazione:</td>
<td style="width: 196px"> Scegli la regione:</td>
</tr>
<tr>
<td style="width: 136px">
<asp:DropDownList ID="ddl1Nazione" runat="server">
<asp:ListItem Selected="True">-- Nazione --</asp:ListItem>
</asp:DropDownList>
</td>
<td style="width: 196px">
<asp:DropDownList ID="ddl2Regione" runat="server">
<asp:ListItem Selected="True">-- Regione --</asp:ListItem>
</asp:DropDownList>
</td>
</tr>
<tr>
<td style="width: 136px"> Scegli la città:</td>
<td style="width: 196px"></td>
</tr>
<tr>
<td style="vertical-align: top; width: 136px">
<asp:DropDownList ID="ddl3Città" runat="server">
<asp:ListItem Selected="True">-- Città --</asp:ListItem>
</asp:DropDownList>
</td>
<td style="width: 196px">
<div id="aggiungiQui"></div>
</td>
</tr>
</table>
...
Nel listato 1 possiamo notare la presenza di un tag <div>
con id="aggiungiQui"
che ci servirà in seguito per posizionare la tabellina che costruiremo sul client.
Per riempire la prima DropDownList
, la ddl1Nazione
, in modo dinamico, basta fare una lettura sul database Immobiliare.MDF
e scrivere le nazioni contenute nell'unica tabella presente, la tabella Appartamenti
. Quindi inseriremo nella Page_Load
una chiamata al metodo Imposta_ddl1Nazione()
:
Listato 2. Metodo Imposta_ddl1Nazione()
protected void Imposta_ddl1Nazione()
{
SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["AppConnStr"].ConnectionString);
SqlDataAdapter adapter = new SqlDataAdapter("SELECT DISTINCT [Nazione] FROM [Appartamenti]", conn);
DataSet ds = new DataSet();
adapter.Fill(ds, "Appartamenti");
foreach (DataRow row in ds.Tables["Appartamenti"].Rows)
ddl1Nazione.Items.Add(row["Nazione"].ToString());
}
Il metodo Imposta_ddl1Nazione()
esegue una semplice SELECT DISTINCT
della colonna Nazione
nella tabella Appartamenti
e, per ogni nazione trovata, aggiunge un elemento a ddl1Nazione
.
Abbiamo la lista con i nomi delle nazioni. Quando l'utente ne sceglie una, la seconda DropDownList
deve essere popolata con le regioni, presenti sul database, che appartengono a quella nazione.
Vogliamo però fare questo evitando il postback, perché non serve ricaricare tutta la pagina per quelle poche informazioni che ci servono.
Abbiamo detto che la pagina che utilizza il Client Callback deve implementare l'interfaccia ICallbackEventHandler
, quindi nella parte codice interno della pagina, scriviamo le tre righe riportate nel listato 3, dove all'interno della classe parziale _Default
abbiamo definito la stringa pubblica callbackStr
, di cui abbiamo già parlato, ed una stringa privata _result
che ci servirà per passare i risultati al client.
Listato 3. Implementazione dell'interfaccia ICallbackEventHandler
public partial class _Default : System.Web.UI.Page, ICallbackEventHandler
{
public string callbackStr;
private string _result;
...
Nel Page_Load
, abbiamo già posizionato la chiamata al metodo per leggere le nazioni. Mettiamo poi un controllo sulle capacità del browser con cui abbiamo a che fare. Se il browser non supporta il Client Callback lanciamo un'eccezione.
Siccome poi le nostre DropDownList
sono gestite dal client, aggiungiamo ad ognuna di esse un attributo che specifica la funzione lato client che gestisce l'evento onChange
.
Listato 4. Implementazione del Page_Load
protected void Page_Load(object sender, EventArgs e)
{
Imposta_ddl1Nazione();
if (!Request.Browser.SupportsCallback)
throw new ApplicationException("Il browser non supporta il Client Callback.");
ddl1Nazione.Attributes.Add("onChange", "LeggiRegioneDalServer()");
ddl2Regione.Attributes.Add("onChange", "LeggiCittàDalServer()");
ddl3Città.Attributes.Add("onChange", "LeggiAppartamentiDalServer()");
callbackStr = Page.ClientScript.GetCallbackEventReference(this, "Command", "CallBackHandler", "context", "onError", false);
}
Le funzioni lato client che abbiamo specificato nel listato 4 andranno poi implementate in Javascript, compresa la CallBackHandler
ed onError
presenti come parametri nel GetCallbackEventReference
.
Passiamo quindi sulla pagina, la Default.aspx
, ed implementiamo la prima funzione LeggiRegioneDalServer()
.
Listato 5. Implementazione di LeggiRegioneDalServer()
<script language="javascript" type="text/javascript">
function LeggiRegioneDalServer()
{
var Command = "1:" + document.forms[0].elements['ddl1Nazione'].value;
var context = new Object();
context.CommandName = "LeggiRegioneDaNazione";
<%=callbackStr %>
}
LeggiRegioneDalServer()
costruisce la variabile Command
che abbiamo utilizzato nel listato 4, come parametro del GetCallbackEventReference
, mettendo un "1:"
all'inizio della stringa in modo che il metodo sul server possa capire che si tratta di questo comando, e poi il valore di ddl1Nazione
selezionato dall'utente.
Definisce poi l'oggetto context
a cui associa il CommandName "LeggiRegioneDaNazione"
. Anche context
è un parametro del GetCallbackEventReference.
L'ultima riga è un'istruzione lato server che inserisce in quel punto il valore della stringa callbackStr
, che ricordiamo contiene il valore impostato con il GetCallbackEventReference
, cioè la funzione WebForm_DoCallback('__Page', Command, CallBackHandler, context, onError, false)
.
Passiamo ora alla parte server, cioè sul codice interno della pagina, ed implementiamo il metodo RaiseCallbackEvent
.
Listato 6. Implementazione di RaiseCallbackEvent
public void RaiseCallbackEvent(string eventArgument)
{
// se il comando arriva dal primo controllo
// dobbiamo riempire il secondo
if (eventArgument.StartsWith("1:"))
{
// imposto il primo item
_result = "-- Regione --;";
// prendo la stringa dalla posizione 3
eventArgument = eventArgument.Substring(2);
if (eventArgument != "-- Nazione --")
{
// leggo il database
SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["AppConnStr"].ConnectionString);
SqlDataAdapter adapter = new SqlDataAdapter("SELECT DISTINCT [Regione] FROM [Appartamenti] WHERE ([Nazione] = @Nazione)", conn);
DataSet ds = new DataSet();
adapter.SelectCommand.Parameters.Add("@Nazione", SqlDbType.NChar, 10).Value = eventArgument;
adapter.Fill(ds, "Appartamenti");
// costruisco la stringa da restituire
foreach (DataRow row in ds.Tables["Appartamenti"].Rows)
_result += row["Regione"].ToString() + ";";
}
}
...
RaiseCallbackEvent
controlla i primi due caratteri del parametro eventArgument
per capire di che cosa si tratta. Poi prende la stringa che comincia dalla posizione tre, che contiene il valore della nazione, con cui dobbiamo interrogare il database, ed effettuata la query. Ricevuti i valori, costruisce la stringa da restituire al client, che contiene i nomi delle regioni separati dal punto e virgola.
Per restituire la stringa _result
al client ci pensa il metodo GetCallbackResult
che dobbiamo implementare per rispettare l'interfaccia ICallbackEventHandler
.
Listato 7. Implementazione di GetCallbackResult
public string GetCallbackResult()
{
if (_result == "error")
throw new Exception("Selezione non valida!");
else
return _result;
}
GetCallbackResult
lancia un'eccezione se si trova di fronte ad un errore, altrimenti restituisce la stringa.
Torniamo ora sulla parte client, dove dobbiamo implementare, in Javascript, una funzione che decodifichi la stringa che arriva dal server ed esegua le azioni richieste. La funzione si deve chiamare CallBackHandler
come abbiamo impostato sul metodo GetCallbackEventReference
.
Listato 8. Implementazione di CallBackHandler
function CallBackHandler(result, context)
{
if (context.CommandName == "LeggiRegioneDaNazione")
{
document.forms[0].elements['ddl2Regione'].options.length = 0;
while (result.length > 0)
{
var indexofSep = result.indexOf(";");
var itemList = result.substring(0, indexofSep);
result = result.substring(indexofSep + 1);
document.forms[0].elements['ddl2Regione'].options.add(new Option(itemList, itemList));
}
document.forms[0].elements['ddl3Città'].options.length = 0;
document.forms[0].elements['ddl3Città'].options.add(new Option("-- Città --", "-- Città --"));
rimuoviRabellaRisultati();
}
...
CallBackHandler
controlla il CommandName
da cui è partita la richiesta client-server, quindi sa che sta ricevendo una risposta per la richiesta "LeggiRegioneDaNazione"
.
Decodifica poi la stringa che, come abbiamo visto, contiene valori separati dal punto e virgola, e, per ogni valore ricevuto, aggiunge dinamicamente un item con quel valore a ddl2Regione
.
Il listato 8 contiene altri dettagli, che sono di facile comprensione avendo una visione generale dell'applicazione.
Dobbiamo inoltre implementare la funzione di errore sul client che gestirà le eccezioni lanciate dal server.
Listato 9. Implementazione della funzione di errore sul client
function onError(message, context)
{
alert("Exception :n" + message);
}
È ovvio che il nome onError
deve coincidere con il parametro di errore del GetCallbackEventReference.
Allo stesso modo di LeggiRegioneDalServer()
possiamo implementare LeggiCittàDalServer()
.
L'ultima lettura che dobbiamo fare sul server è quella degli appartamenti avendo selezionato la città. Poi con i dati restituiti dobbiamo costruire dinamicamente sul client una tabellina.
Sul RaiseCallbackEvent
dobbiamo aggiungere questa possibilità: se il comando della richiesta è il numero 3, preleviamo il nome della città ed interroghiamo il database. Costruiamo poi la stringa da restituire con i campi letti a coppie di due, ma sempre separati dal punto e virgola.
Listato 10. Implementazione sul RaiseCallbackEvent
...
else if (eventArgument.StartsWith("3:"))
{
// prendo la stringa dalla posizione 3
eventArgument = eventArgument.Substring(2);
if (eventArgument != "-- Città --")
{
// leggo il database
SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["AppConnStr"].ConnectionString);
SqlDataAdapter adapter = new SqlDataAdapter("SELECT [Locali], CAST([Prezzo in Euro] AS decimal(8, 2)) AS Prezzo FROM [Appartamenti] WHERE [Città] = @Città", conn);
DataSet ds = new DataSet();
adapter.SelectCommand.Parameters.Add("@Città", SqlDbType.NChar, 10).Value = eventArgument;
adapter.Fill(ds, "Appartamenti");
// costruisco la stringa da restituire
foreach (DataRow row in ds.Tables["Appartamenti"].Rows)
_result += row["Locali"].ToString() + ";" + "€ " + row["Prezzo"].ToString() + ";";
}
}
...
Quindi sul client dovremmo implementare all'interno del CallBackHandler
il seguente pezzo di codice in cui verifichiamo il comando ed eliminiamo la tabella vecchia con rimuoviRabellaRisultati()
. Se ci sono risultati inizializziamo una nuova tabella con la funzione aggiungiTabellaRisultati()
. Il resto del codice serve a costruire la tabella con la stringa che ci ritorna dal server, tenendo presente che i campi sulla stringa sono a coppie di due e tutti separati dal punto e virgola.
Listato 11. Implementazione sul CallBackHandler
...
if (context.CommandName == "LeggiAppartamenti")
{
rimuoviRabellaRisultati();
if (result.length > 0)
{
aggiungiTabellaRisultati();
var tabella = document.getElementById("tabellaRisultati");
var i = 0; var j = 0;
}
while (result.length > 0)
{
var indexofSep = result.indexOf(";");
var itemList = result.substring(0, indexofSep);
result = result.substring(indexofSep + 1);
if (j == 0)
tabella.insertRow(i + 1);
tabella.rows[i + 1].insertCell(j);
tabella.rows[i + 1].cells[j].innerHTML = itemList;
if (j < 1)
j = j + 1;
else
{
i = i + 1;
j = 0;
}
}
}
...
Per completezza riportiamo i listati delle funzioni Javascript per inizializzare la tabella e per cancellarla.
Listato 12. Implementazione di aggiungiTabellaRisultati() sul client
function aggiungiTabellaRisultati()
{
var theTable = document.createElement("table");
theTable.setAttribute("id", "tabellaRisultati");
theTable.setAttribute("border", "2");
theTable.style.cssText = "font-weight: bold; text-align: center; background-color: whitesmoke;";
theTable.insertRow(0);
theTable.rows[0].insertCell(0); theTable.rows[0].insertCell(1);
theTable.rows[0].cells[0].innerHTML = "Locali";
theTable.rows[0].cells[1].innerHTML = "Prezzo";
theTable.rows[0].style.cssText = "background-color: darkblue; color: white;";
theTable.rows[0].cells[0].style.cssText = "padding-left: 20px; padding-right: 20px;";
theTable.rows[0].cells[1].style.cssText = "padding-left: 20px; padding-right: 20px;";
var insertSpot = document.getElementById("aggiungiQui");
insertSpot.appendChild(theTable);
}
Nel listato 12 notiamo che la tabella viene aggiunta all'interno del tag con id aggiungiQui
che avevamo visto all'inizio. Sono inoltre presenti delle istruzioni per impostare gli stili css per la tabella.
Listato 13. Implementazione di rimuoviRabellaRisultati() sul client
function rimuoviRabellaRisultati()
{
var insertSpot = document.getElementById("aggiungiQui");
if (insertSpot.hasChildNodes())
insertSpot.removeChild(insertSpot.lastChild);
}
A questo punto possiamo testare la nostra applicazione.
La selezione della nazione provoca la formazione della lista delle regioni.
Selezionando la regione riempiamo la lista delle città.
Selezionando la città otteniamo la tabellina degli appartamenti.
Il tutto avviene senza postback con un incremento notevole delle prestazioni.
Il sorgente dell'esempio sviluppato in questo articolo può essere scaricato da qui ed è stato testato su Internet Explorer 6.0 e Firefox 1.5.