Per chiunque fosse interessato, segnalo l’apertura di un nostro account su CodeCanyon, il marketplace online di Envato, dedicato alla programmazione.
Attualmente contiene due soli controlli, ma altri e più interessanti sono già in lavorazione, seguiteci!
Post Taggati ‘Asp.Net’
download esempio
In questo articolo illustreremo come costruire un handler in grado di manipolare un’immagine richiesta dal client.
Il problema
Quando inseriamo un’immagine (tipicamente la miniatura di un’immagine più grande) in una pagina web spesso riduciamo la dimensione dell’immagine utilizzando le proprietà css width e height
<img src="immagine.jpg" alt="bla bla" style="width:200px; height: 200px;" />
il che porta al resize operato dal browser che, più o meno, è una schifezza.
L’alternativa è produrre la miniatura utilizzando programmi come Photoshop e Gimp, che scalano e ricampionano correttamente l’immagine.
Spesso però non è possibile costruirsi a mano la miniatura. Innanzitutto perchè è una rottura, e quantomeno duplica il numero di immagini da gestire nel nostro markup. Spesso non possiamo nemmeno sapere a priori quali saranno le immagini, ad esempio nella visualizzazione di gallerie fotografiche uppate da altri utenti.
La soluzione
Asp.Net ci viene in aiuto con la tecnica degli HttpHandler generici.
Senza dilungarci troppo nella presentazione degli handler diciamo semplicemente che un handler è una parte di codice che si occupa di intercettare una particolare richiesta inviata al server e di costruire una risposta adatta.
Possiamo quindi immaginare di costruire una classe che implementa l’interafccia IHttpHandler, in grado di processare un’immagine richiesta in base ad alcuni parametri e riversare nello stream di output una versione modificata della stessa immagine (ovviamente senza modificare l’immagine originale).
La richiesta potrebbe includere una semplice scalatura dell’immagine, un ritaglio di una particolare sezione, l’arrotondamento degli angoli, ecc…
Veniamo al codice
La costruzione di un handler prevede 2 passi fondamentali:
- Scrivere la classe
- Aggiungere l’handler all’insieme degli handlers del nostro sito web.
Un primo abbozzo della classe (che chiameremo imghandler) potrebbe essere il seguente:
namespace Recoding.Web
{
using System;
using System.Web;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
public class imghandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
}
public bool IsReusable { get { return false; } }
}
}
Il metodo ProcessRequest è il cuore dell’handler e conterrà il comportamento da seguire per soddisfare la richiesta.
I namespace System.Drawing, System.Drawing.Imaging e System.Drawing.Drawing2D costituiscono l’accesso alle librerie GDI+ che saranno indispensabili per la manipolazione dell’immagine (per una panoramica generale leggete quì).
Ora dobbiamo definire l’handler nel web.config del nostro sito web, per fare in modo che le richieste specifiche vengano dirottate a questa classe:
<httpHandlers>
<remove verb="*" path="*.asmx"/>
<add verb="*" path="imghandler.ashx" type="Recoding.Web.imghandler"/>
</httpHandlers>
In questo modo ogni richiesta alla risorsa imghandler.ashx passerà per il nostro ProcessRequest, e saremo in grado di manipolarla.
Definizione dei parametri
Come prima cosa dobbiamo definire cosa l’handler è in grado di accettare come richiesta.
Prevediamo quindi un set di costanti e attributi:
...
public class imghandler : IHttpHandler
{
public static string _PARAM_IMAGEPATH = "imagepath";
public static string _PARAM_WIDTH = "width";
public static string _PARAM_HEIGHT = "height";
public static string _PARAM_CROP = "crop";
public static string _PARAM_VERTICALALIGN = "valign";
public static string _PARAM_HORIZONTALALIGN = "halign";
public static string _PARAM_DEFAULTIMAGEPATH = "defaultimagepath";
public static string _PARAM_ROUNDED = "rounded";
public static string _PARAM_RADIUS = "radius";
public string imagepath { get; set; }
public string defaultimagepath { get; set; }
public string physical_imagepath { get; set; }
private string imgExtension { get; set; }
public int width { get; set; }
public int height { get; set; }
public bool crop { get; set; }
public string valign { get; set; }
public string halign { get; set; }
public bool rounded { get; set; }
public int radius { get; set; }
public void ProcessRequest(HttpContext context) { ... }
...
Con questi parametri una possibile richiesta al nostro handler potrebbe essere costruita nella forma:
<img alt="" src="imghandler.ashx?imagepath=immagine1.jpg&width=200&height=200" />
ad indicare che vogliamo ottenere l’immagine1.jpg scalata (o croppata) a 200 pixel per 200.
Ora possiamo procedere alla scrittura del corpo del metodo ProcessRequest, che come prima cosa dovrà ricavare i parametri passati in querystring, dopodichè procederà all’elaborazione vera e propria, che demanderemo ad altri 2 metodi:
public void ProcessRequest(HttpContext context)
{
string sImg = context.Request.QueryString[_PARAM_IMAGEPATH];
physical_imagepath = context.Server.MapPath(sImg.StartsWith("~/") ? sImg : "~/" + sImg);
width = GenericConverter.TryGetValue<int>(context.Request.QueryString[_PARAM_WIDTH], 0);
height = GenericConverter.TryGetValue<int>(context.Request.QueryString[_PARAM_HEIGHT], 0);
crop = GenericConverter.TryGetValue<bool>(context.Request.QueryString[_PARAM_CROP], false);
rounded = GenericConverter.TryGetValue<bool>(context.Request.QueryString[_PARAM_ROUNDED], false);
radius = GenericConverter.TryGetValue<int>(context.Request.QueryString[_PARAM_RADIUS], 8);
defaultimagepath = "";
if (context.Request.QueryString[_PARAM_DEFAULTIMAGEPATH] != null)
defaultimagepath = context.Request.QueryString[_PARAM_DEFAULTIMAGEPATH].ToString();
try
{
FileInfo fileImage = new FileInfo(physical_imagepath);
if (!fileImage.Exists)
{
sImg = defaultimagepath;
physical_imagepath = context.Server.MapPath(sImg.StartsWith("~/") ? sImg : "~/" + sImg);
}
}
catch
{
sImg = defaultimagepath;
physical_imagepath = context.Server.MapPath(sImg.StartsWith("~/") ? sImg : "~/" + sImg);
}
// Forzo l'estenzione a png nel caso sia richiesta la stondatura dell'immagine
// ciò mi consente di avere lo sfondo trasparente nei punti non coperti
imgExtension = rounded ? ".PNG" : Path.GetExtension(sImg);
ProcessImage(context);
}
Come si può vedere, la prima operazione è costruire il percorso fisico dell’immagine nel server, se l’immagine non viene trovata si utilizzerà un’immagine di default rappresentata dal parametro _PARAM_DEFAULTIMAGEPATH, che potrebbe ad esempio essere il logo del sito.
Al termine della raccolta parametri viene invocato il metodo ProcessImage, che si occuperà dell’effettiva elaborazione.
NOTA: la classe GenericConverter è un helper contenente dei metodi in grado di tentare una conversione di un dato in un certo tipo, se la conversione riesce viene restituito il valore, altrimenti viene restituito un valore di default passato come parametro. Ho sviluppato la classe all’interno del namespace Recoding.Convert, che occorre aggiungere tra gli using della classe. Il codice dell’helper è il seguente, è piuttosto banale e non mi dilungherò in spiegazioni:
namespace Recoding.Convert
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
public class GenericConverter
{
public static T TryGetValue<T>(object p, object defaultValue) where T : IConvertible
{
return TryGetValue<T>(p, defaultValue, CultureInfo.CurrentCulture);
}
public static T TryGetValue<T>(object p, object defaultValue, IFormatProvider fp) where T : IConvertible
{
try
{
return (T)Convert.ChangeType(p, typeof(T), fp);
}
catch
{
return (T)defaultValue;
}
}
}
}
ProcessImage
Uno stub iniziale del metodo prevederà la creazione di una Bitmap dal percorso fisico del file richiesto (indicato nell’attributo physical_imagepath) e si dividerà in due tronconi: la scalatura dell’immagine e il suo ritaglio. La strada da intraprendere viene indicata dal valore dell’attributo crop.
...
public void ProcessImage(HttpContext context)
{
System.Drawing.Image img = System.Drawing.Image.FromFile(physical_imagepath);
// Bitmap di partenza
Bitmap original_image = (Bitmap)img;
// Seleziono il metodo di elaborazione
if (crop)
{
#region -- crop --
// TODO
#endregion
}
else
{
#region -- scalatura --
// TODO
#endregion
}
}
...
Una piccola nota sul processo di crop.
Solitamente il crop porta ad una selezione di un’area all’interno dell’immagine che può anche essere separata da tutti e 4 i bordi.
Immagini gentilmente concesse da Riccardo Vandoni
Il crop che ho deciso di implementare, invece, ridimensiona l’immagine per far si che almeno una coppia di bordi tocchi sempre l’area selezionata.
In sostanza se abbiamo un’immagine di w500 e h200 e decidiamo di visualizzare una miniatura croppata di w100 e h100 l’handler scalerà proporzionalmente l’immagine per ottenere una miniatura alta 100 e larga 250, taglierà fuori i 150 pixel in eccesso, posizionando il viewport orizzontalmente sulla base del parametro halign.

Ho preferito questo approccio perchè l’handler è stato concepito per la visualizzazione di miniature di foto, e non avrebbe senso selezionare un’area all’interno della foto senza eseguire una scalatura, col rischio quindi di inquadrare un’area priva di significato.
Non escludo comunque la possibilità di implementare in futuro l’altra possibilità.
La scalatura
Ora vediamo come scalare l’immagine, la cosa più semplice.
Partiamo dal presupposto che la scalatura può essere effettuata in 2 modi: modificando una delle due dimensioni dell’immagine e ricalcolando la restante, lasciando invariato il rapporto base/altezza, oppure ridefinendo entrambe le dimensioni, e ciò può portare ad uno stretch dell’immagine.
...
#region -- scalatura --
if (width > 0 || height > 0)
{
//se solo height=0, l'altezza viene ridotta proporzionalmente alla larghezza
if (height == 0)
height = Convert.ToInt32(((float)(width) / (float)(original_image.Width)) * original_image.Height);
//se solo width=0, l'altezza viene ridotta proporzionalmente alla larghezza
if (width == 0)
width = Convert.ToInt32(((float)(height) / (float)(original_image.Height)) * original_image.Width);
//Ora viene creata la thumbnail e salvata in un output di stream
using (Bitmap oThumb = new Bitmap(original_image, width, height))
{
if (rounded)
{
TextureBrush texture = new TextureBrush(oThumb);
Graphics graphic = Graphics.FromImage(oThumb);
graphic.Clear(Color.Transparent);
graphic.CompositingQuality = CompositingQuality.HighQuality;
graphic.SmoothingMode = SmoothingMode.HighQuality;
graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphic.FillRoundedRectangle(texture, 0, 0, width, height, radius);
graphic.Save();
}
else
{
using (Graphics oGraphic = Graphics.FromImage(oThumb))
{
oGraphic.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
oGraphic.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
oGraphic.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
Rectangle oRectangle = new Rectangle(0, 0, width, height);
oGraphic.DrawImage(original_image, oRectangle);
}
}
//salvo in uno stream di output
StreamOut(oThumb, context);
}
}
else
{
// Se width e height sono entrambi uguali a 0, l’immagine viene salvata nello stream cosi com’è
StreamOut(original_image, context);
}
#endregion
...
Come si può vedere la prima cosa che facciamo è controllare il valore dei due attributi width e height e ricalcolare se necessario l’attributo mancante.
Se nessuno dei due è valorizzato il processo viene bypassato e verrà restituita in output l’immagine originale.
Se sono presenti dei valori l’immagine viene alterata (proporzionalmente o no), viene quindi generato un oggetto Bitmap che rappresenta la nuova miniatura.
Viene fatto un ulteriore controllo: se l’attributo rounded è uguale a true si procede alla costruzione degli angoli stondati.
Per fare ciò mi sono avvalso del fantastico namespace Plasmoid, sviluppato da Arun Reginald, e che trovate pubblicato su CodeProject a questo indirizzo.
Il namespace contiene dei metodi di estenzione del namespace System.Drawing, uno dei quali è appunto FillRoundedRectangle.
Non mi dilungherò sull’utilizzo delle librerie GDI+, una completa documentazione la trovate all’indirizzo indicato precedentemente.
Il crop
Per quel che riguarda il ritaglio dell’immagine le cose si complicano un pò.
Se ad esempio la miniatura che vogliamo generare ha un rapporto base/altezza diverso da quello dell’immagine originale avremo una delle due dimensioni “esuberante” rispetto alla superficie visualizzata, dovremo operare quindi un riallineamento del viewport.
Potremmo ad esempio voler sempre visualizzare la parte centrale, la sinistra, la destra, e così via, escludendo ciò che eccede il nostro nuovo formato.
Per definire questi allineamenti ci avvarremo degli enumeratori VerticalAlign e HorizontalAlign, comuni ai WebControl, i cui valori saranno intercettati dai parametri _PARAM_VERTICALALIGN e _PARAM_HORIZONTALALIGN.
Il codice della nostra sezione crop sarà il seguente:
...
#region -- crop --
// Se una delle due dimensioni è mancante le rendo uguali
if (width == 0 || height == 0) { width = height = Math.Max(width, height); }
using (Bitmap output_image = new Bitmap(width, height))
{
// Ricavo dalla request gli allineamenti richiesti.
// Ovviamente ha senso solo per le immagini croppate.
valign = context.Request.Params[_PARAM_VERTICALALIGN] ?? VerticalAlign.NotSet.ToString();
halign = context.Request.Params[_PARAM_HORIZONTALALIGN] ?? HorizontalAlign.NotSet.ToString();
int new_width, new_height;
// Calcolo il ratio dell'immagine originale e quello dell'immagine di destinazione
// Mi occorrono per capire in che asse l'immagine originale risulta esuberante rispetto alla nuova proporsione
float target_ratio = (float)width / (float)height;
float image_ratio = (float)original_image.Width / (float)original_image.Height;
if (target_ratio < image_ratio)
{
new_height = height;
new_width = (int)Math.Floor(image_ratio * (float)height);
}
else
{
new_height = (int)Math.Floor((float)width / image_ratio);
new_width = width;
}
new_width = new_width > width ? new_width : width;
new_height = new_height > height ? new_height : height;
#region -- Offset degli assi, assegno i valori di default (NotSet e Middle/Center) --
int paste_x = (width - new_width) / 2;
int paste_y = (height - new_height) / 2;
// Applico gli allineamenti verticali
switch ((VerticalAlign)Enum.Parse(typeof(VerticalAlign), valign))
{
case VerticalAlign.NotSet:
case VerticalAlign.Middle:
// bypass
break;
case VerticalAlign.Top:
paste_y = 0;
break;
case VerticalAlign.Bottom:
paste_y = height - new_height;
break;
}
// Applico gli allineamenti orizzontali
switch ((HorizontalAlign)Enum.Parse(typeof(HorizontalAlign), halign))
{
case HorizontalAlign.NotSet:
case HorizontalAlign.Center:
// bypass
break;
case HorizontalAlign.Left:
paste_x = 0;
break;
case HorizontalAlign.Right:
paste_x = width - new_width;
break;
}
#endregion
if (rounded)
{
Bitmap thumb = new Bitmap(width, height);
Graphics gthumb = Graphics.FromImage(thumb);
gthumb.CompositingQuality = CompositingQuality.HighQuality;
gthumb.SmoothingMode = SmoothingMode.HighQuality;
gthumb.InterpolationMode = InterpolationMode.HighQualityBicubic;
gthumb.DrawImage(original_image, paste_x, paste_y, new_width, new_height);
gthumb.Save();
TextureBrush texture = new TextureBrush(thumb);
Graphics graphic = Graphics.FromImage(output_image);
graphic.CompositingQuality = CompositingQuality.HighQuality;
graphic.SmoothingMode = SmoothingMode.HighQuality;
graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphic.FillRoundedRectangle(texture, 0, 0, width, height, radius);
graphic.Save();
}
else
{
Graphics graphic = Graphics.FromImage(output_image);
graphic.CompositingQuality = CompositingQuality.HighQuality;
graphic.SmoothingMode = SmoothingMode.HighQuality;
graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphic.DrawImage(original_image, paste_x, paste_y, new_width, new_height);
graphic.Save();
}
// Stream out dell'immagine
StreamOut(output_image, context);
}
#endregion
...
StreamOut
Rimane quindi da definire il metodo StreamOut, che si occuperà di restituire al client l’immagine elaborata, concordamente al suo formato:
public void StreamOut(Bitmap bitmap, HttpContext context)
{
switch (imgExtension.ToUpper())
{
case ".JPEG":
case ".JPG":
bitmap.Save(context.Response.OutputStream, ImageFormat.Jpeg);
break;
case ".GIF":
bitmap.Save(context.Response.OutputStream, ImageFormat.Gif);
break;
case ".PNG":
using (MemoryStream stream = new MemoryStream())
{
bitmap.Save(stream, ImageFormat.Png);
stream.WriteTo(context.Response.OutputStream);
}
break;
}
}
Poichè la png richiede uno stream bidirezionale è necessario eseguire il suo rendering in un MemoryStream, che verrà poi riversato nel Response.OutputStream.
A questo punto abbiamo un handler che può intercettare richieste di vario genere.
Se ad esempio volessimo un’immagine ritagliata a 300 pixel per 300, con angoli stondati di 10 pixel di raggio avremmo la richiesta:
<img alt="" src="imghandler.ashx?imagepath=image.jpg&width=300&height=300&crop=true&rounded=True&radius=10" />
Se volessimo ottenere un’immagine scalata all’altezza di 200 pixel e ricalcolata proporzionalmente scriveremo:
<img alt="" src="imghandler.ashx?imagepath=image.jpg&height=200&crop=false&rounded=false" />
Ulteriori esempi sono presenti in un demo funzionante scaricabile quì.
In un futuro articolo illustrerò come incapsulare quest’handler in un custom control, rendendo più semplice ed elegante il suo utilizzo.
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?

