Non molto tempo fa mi sono trovato nella situazione di dover gestire il bind di una lista di elementi in maniera “mista”, ossia dovevo effettuare un primo bind lato server sulla base dei record di un database, e consentire poi all’utente finale di modificare lato client tale lista.
Il progetto in questione era in .NET, avrei potuto effettuare il bind lato server con un asp:Repeater o un qualsiasi altro controllo bindabile.
Lato client avrei potuto utilizzare jQuery per gestire l’input dell’utente rigenerando l’html degli elementi aggiunti, con tutte le chiamate ajax del caso per il salvataggio dei dati sul DB.
Il problema più grosso di questo approccio è che il template utilizzato nel controllo .NET (tipicamente l’ItemTemplate del Repeater) non è esportabile lato client, ciò significa che se avessi dovuto modificare la struttura degli elementi della lista l’avrei dovuto fare su entrambi i fronti, client e server, mantenendoli allineati.
Il bind lato client, poi, avrebbe dovuto essere effettuato allo stesso modo del server, quindi al disallineamento del template si sarebbe aggiunto un eventuale disallineamento delle logiche sui dati.
Non so se la soluzione da me trovata è già standardizzata né pretendo che sia la soluzione a tutti i problemi, analizzeremo in seguito i difetti di tale approccio, ma di sicuro ha una sua eleganza e portabilità.
Questa soluzione “cross” si basa fondamentalmente sull’utilizzo di XML per i dati e di fogli XSL per produrre frammenti di HTML partendo da tali dati, più alcuni accorgimenti per rendere il tutto il più automatico possibile.
Lato server
Per il nostro esempio (scaricabile) ci baseremo su una semplice lista anagrafica modellata dalle seguenti classi:
public class Anagrafiche
{
public List<Persona> Persone { get; set; }
}
public class Persona
{
public string Nome { get; set; }
public string Cognome { get; set; }
public string Comune { get; set; }
public Persona() { }
}
Avremo quindi la necessità di presentare una lista di persone e di consentire all’utente di aggiungerne altre, evitando fastidiosi postback e l’utilizzo degli altrettanto fastidiosi UpdatePanel :)
La pagina .aspx contiene un controllo
<asp:Xml ID="xmlData" runat="server" />
che consente di renderizzare un documento XML (o un frammento XHTML, nel nostro caso) passatogli, effettuando eventualmente una trasformazione sui dati.
Avremo poi 2 fogli .XSL per effettuare le trasformazioni, che funzioneranno da template. Il primo è il contenitore della lista, persone.xsl:
<?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:include href="persona.xsl" /> <xsl:template match="/"> <div class="persone"> <span class="title">Lista utenti:</span> <xsl:apply-templates select="//Persona" /> <div style="clear:both;"></div> </div> </xsl:template> </xsl:stylesheet>
il secondo rappresenta invece l’elemento dell’anagrafica, persona.xsl:
<?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="Persona"> <div class="persona"> <span> <xsl:value-of select="Nome" /> </span> <xsl:text xml:space="preserve"> </xsl:text> <span> <xsl:value-of select="Cognome" /> </span> <span id="comune"> Comune: <xsl:value-of select="Comune" /> </span> </div> </xsl:template> </xsl:stylesheet>
Nell’esempio creo la lista delle anagrafiche a mano, ovviamente i dati possono avere qualsiasi provenienza
Anagrafiche anag = new Anagrafiche();
anag.Persone = new List<Persona>();
anag.Persone.Add(new Persona() { Nome = "Mario", Cognome = "Rossi", Comune = "FantasiaPortamiVia" });
anag.Persone.Add(new Persona() { Nome = "Luigi", Cognome = "Verdi", Comune = "ComeSopra" });
Ora dobbiamo ottenere un documento XML dall’oggetto anag, fortunatamente ci viene in aiuto la serializzazione del .NET.
Modifichiamo le classi Anagrafiche e Persona, aggiungendo i seguenti attributi:
using System.Xml;
using System.Xml.Serialization;
...
public class Anagrafiche
{
[XmlArray("Persone"), XmlArrayItem(typeof(Persona), ElementName = "Persona")]
public List<Persona> Persone { get; set; }
}
[XmlRoot("Persona")]
public class Persona
{
...
}
Ciò ci consentirà, tramite un XmlSerializer di ottenere un documento nel formato richiesto dai fogli XSL.
A questo punto possiamo già realizzare il bind lato server:
protected void Bind_Xml(Anagrafiche anag)
{
// Serializzatore xml, sulla base della classe Anagrafiche
XmlSerializer ser = new XmlSerializer(typeof(Anagrafiche));
string sXml = "";
// Creo un memorystream in cui riversare la stringa dell'xml, occhio allo using
using (MemoryStream ms = new MemoryStream())
{
ser.Serialize(ms, anag);
ms.Seek(0, SeekOrigin.Begin);
StreamReader sr = new StreamReader(ms);
sXml = sr.ReadToEnd();
}
if (sXml != "")
{
// Creo un XslTransform.
// La classe è deprecata, in favore di XslCompiledTransform, ma per un piccolo esempio come questo andrà più che bene,
// sembra anche più semplice da usare.
XslTransform xsltRiepilogo = new XslTransform();
xsltRiepilogo.Load(Server.MapPath("~/xslt/persone.xsl"));
// Assegno il foglio xslt all'asp:Xml
xmlData.Transform = xsltRiepilogo;
// Assegno il documento xml al controllo, che in congiunzione con l'xslt produrrà l'html di cui ho bisogno.
xmlData.DocumentContent = sXml;
xmlData.DataBind();
}
}
la parte a mio avviso più interessante è il processo automatico (con l’utilizzo dell’XmlSerializer) attraverso cui otteniamo il documento XML necessario, che per il nostro esempio sarà:
<?xml version="1.0"?>
<Anagrafiche xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Persone>
<Persona>
<Nome>Mario</Nome>
<Cognome>Rossi</Cognome>
<Comune>FantasiaPortamiVia</Comune>
</Persona>
<Persona>
<Nome>Luigi</Nome>
<Cognome>Verdi</Cognome>
<Comune>ComeSopra</Comune>
</Persona>
</Persone>
</Anagrafiche>
Da notare il fatto che il documento ha appunto il formato dichiarato nelle classi da cui deriva [XmlArray("Persone") ...] e [XmlRoot("Persona")] ci hanno consentito di definire i tag di output.
Questo documento sarà quindi assegnato come stringa a sXml, a sua volta assegnato a xmlData.DocumentContent, come documento XML da elaborare.
La riga
xmlData.Transform = xsltRiepilogo;
definisce invece con quale foglio si vogliono processare i dati.
Basta un’infarinatura di XPath e XSLT per capire che l’output sarà un frammento HTML del tipo:
<div class="persone">
<span class="title">Lista utenti:</span>
<div class="persona">
<span>Mario</span>
<span>Rossi</span>
<span id="comune"> Comune: FantasiaPortamiVia</span>
</div>
<div class="persona">
<span>Luigi</span>
<span>Verdi</span>
<span id="comune"> Comune: ComeSopra</span>
</div>
<div style="clear: both;" />
</div>
Et voilà, abbiamo la prima parte dell’opera.
Lato client
Come dicevamo, vogliamo fare in modo che la lista possa crescere in modo asincrono tramite input inserito dall’utente.
Per far ciò utilizzeremo jQuery, più l’ottimo plug-in Transform che trovate a quest’indirizzo http://plugins.jquery.com/project/Transform, e che effettua lato client (e cross-browser, importante) le stesse operazioni della classe .NET XslTransform.
Come prima cosa dobbiamo configurare un web service che possa gestire le chiamate client, con un metodo che accetti in input nome, cognome e comune, e che ci restituisca quantomeno l’XML necessario ad effettuare la trasformazione con jQuery.
Ovviamente potremmo non passare dal server, tutto dipende da cosa nel dettaglio debba fare la nostra applicazione, ma c’è almeno un buon motivo per farlo: potremo utilizzare la serializzazione .NET su un elemento Persona, creato in base all’input dell’utente, ottenendo automaticamente l’XML che ci occorre per essere trasformato, questa volta non da persone.xsl, ma da persona.xsl.
E questo spiega anche perchè abbiamo creato 2 fogli anzichè uno solo…
Nel mio caso, poi, il passaggio per il web service era obbligato dalla necessità di salvare il record nel database.
Quindi, una volta creato il Web Service assicuriamoci che abbia i seguenti using e attributi:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Script.Services;
using System.Xml.Serialization;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.IO;
[ScriptService]
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class WebService : System.Web.Services.WebService {
...
}
A questo punto creiamo una classe per le risposte al client:
[DataContract]
public class jsonResult
{
[DataMember]
public bool result { get; set; }
[DataMember]
public string xmlItem { get; set; }
}
Gli attributi [DataContract] e [DataMember] ci consentiranno di serializzare in json l’oggetto. Comodo no?
Il metodo per l’aggiunta delle persone, che chiameremo inserisci, è il seguente:
[ScriptMethod(UseHttpGet = false)]
[WebMethod(EnableSession = true)] // Ci serve per poter recuperare la session
public jsonResult inserisci(string nome, string cognome, string comune)
{
bool bResult = true;
string xml = "";
try
{
XmlSerializer ser = new XmlSerializer(typeof(Persona));
Persona nuovaPersona = (new Persona() { Nome = nome, Cognome = cognome, Comune = comune });
using (MemoryStream ms = new MemoryStream())
{
ser.Serialize(ms, nuovaPersona);
ms.Seek(0, SeekOrigin.Begin);
StreamReader sr = new StreamReader(ms);
xml = sr.ReadToEnd();
}
}
catch
{
bResult = false;
}
jsonResult json = new jsonResult() { result = bResult, xmlItem = xml };
return json;
}
L’attributo [ScriptMethod(UseHttpGet = false)] ci permette di effettuare la chiamata al metodo in post.
Nell’html abbiamo intanto creato 3 input text con i seguenti id: txtNome, txtCognome e txtComune; più un button btnAggiungi per eseguire l’inserimento.
Il javascript è nell’esempio notevolmente semplificato, manca ogni tipo di validazione e gestione dell’errore, ma rende comunque l’idea:
$(function() {
$("#btnAggiungi").click( function() {
var v_nome = $("#txtNome").val();
var v_cognome = $("#txtCognome").val();
var v_comune = $("#txtComune").val();
var jsonData = JSON.stringify({ nome: v_nome,
cognome: v_cognome,
comune: v_comune });
$.ajax({
type: "POST",
url: "WebService.asmx/inserisci",
data: jsonData,
contentType: "application/json; charset=utf-8",
success: function(resp) {
var serverResp = JSON.parse(resp);
var json = serverResp.d;
if (json.result)
{
$.transform({
xmlstr: json.xmlItem,
xsl: "xslt/persona.xsl",
async: false,
success: function(html, xsl, xml, obj) {
$(html).insertAfter(".persona:last");
}
});
}
else
{
// Gestione dell'errore
}
},
error: function() { /* Gestione dell'errore */ }
});
});
});
Come possiamo vedere viene gestito l’evento click di btnAggiungi, dove vengono raccolti i dati inseriti dall’utente, e tramite la libreria JSON2 (http://www.json.org/js.html) viene generata una stringa sulla base dell’oggetto che racchiude i dati da postare al metodo inserisci del Web Sevice (da notare che gli attributi hanno lo stesso nome dei parametri del metodo).
Viene poi effettuata una chiamata ajax al web service, postando l’oggetto jsonData.
La funzione di success riceve, nella variabile resp, il risultato della chiamata, ne effettua una conversione in oggetto json tramite JSON.parse (lato server l’avevamo serializzato) e verifica che json.d.result sia true (non si è verificato un errore lato server).
A questo punto utiliziamo il plug-in Transform per elaborare json.d.xmlItem che come abbiamo visto contiene l’XML della nuova persona, selezionando come foglio persona.xsl.
Nel success di $.transform, infine, effettuiamo l’append dell’html così ottenuto.
E questa è la fine del giro.
Abbiamo effettuato un bind che si potrebbe definire “cross”, svincolandoci dai template del .NET e creando una soluzione altamente portabile, ligia agli standard (XML, XPath e XSLT) e piuttosto flessibile.
Ovviamente ci sono anche i contro.
- Innanzitutto questo esempio funziona bene con controlli html, ma se volessimo utilizzare controlli Asp.Net nel template XSLT le cose si complicherebbero notevolmente.
- Eventuali paginazioni gestite lato server sono inutilizzabili, al crescere degli elementi dal client non ho controllo sul paginatore.
- Infine non ho idea delle prestazioni generali della soluzione. Come ho già detto la classe XslTransform è stata deprecata in favore della XslCompiledTransform, che risulta più veloce, ma non ho fatto dei test per confrontarle con i template di un controllo bindabile…
Diciamo semplicemente che può essere un interessante esperimento.
Qualche commento, suggerimento o errori da segnalare?

download esempio