Massimiliano's Blog - My .Net World

Tutto quello che avreste voluto sapere ma non avete mai chiesto......
HTML5 (Web Socket)

Salve a tutti, finalmente ho trovato 5 min. di tempo per aggiornare un pò il mio blog, in questo senso volevo condividere con Voi un post che a sua vota mi è stato segnalato e che reputo veramente interessante in ottica futura (silverliight e HTML5). In particolare viene affrontato il concetto delle web socket implementate in HTML5 e supportate in silverlight .

Al seguente url potete vedere la classica chat "long polling" http://40interop.ep.interop.msftlabs.com/html5/wschat.html

Invece questo è il post originale

http://tomasz.janczuk.org/2010/07/silverlight-html5-websocket-client-with.html

Ciao Ciao

La Macchina di Turing….

Girovagando per la rete in cerca di risorse utili per Workflow Foundation 4.0 un mio collega mi ha mostrato una cosa davvero strabiliante che non posso fare a meno di condividere…. ebbene si la macchina di Turing implementata fisicamente e controllata da un Flow Chart di Workflow Foundation 4.0. Eccovi il link del blog

http://www.johandekoning.nl/index.php/2010/04/07/turing-machine-implementation-with-wf4-and-flowchart/

Enjoy yourself

Posted: 19 mag 2010 0:26 da Massimiliano | con no comments
Inserito sotto:
API di Bing: Selezione multipla dei pushpins

Salve a tutti, per un nuovo progetto interessantissimo a cui ho preso parte da poco, mi è stato chiesto di rappresentare delle informazioni su una cartina geografica, ovvero geo-localizzare degli indirizzi e creare un qualcosa che permetta di visualizzare delle informazioni necessarie agli operatori per poi interagire con esse.

Come molti di voi ben sanno il mercato sotto questo punto di vista offre molto, vedi google  o mapPoint etc etc, ma nel nostro caso abbiamo scelto le API di Bing. In dettaglio potete trovare informazioni a questo link. In pratica c’è a disposizione un SDK per interrogare e rappresentare coordinate polari terrestri su di una mappa.  La nostra scelta inoltre è ricaduta sull’SDK per Silverlight, esiste anche uno per asp.net ma con troppo codice javascript a mio avviso. Siccome la geo-localizzazione di un indirizzo avviene tramite chiamate  a webservices è necessario registrarsi per ottenere una licenza da sviluppatore in modo tale da poter interrogare con alcune limitazioni sull’accuratezza dell’indirizzo da ricercare (loro non lo dicono me ne sono accorto con semplici test  :D ); in ambienti di produzione è invece necessario acquistare una licenza che permette chiamate al webservice di geo-localizzazione senza tale limitazione, cmq per maggiori info verificate sempre sul sito microsoft.

Gli esempi dell’SDK sono molto esaustivi per cui tralascio i dettagli di come geo-localizzare un indirizzo concentrandoci sulla gestione dei  pushpins. In primis  creiamo una applicazione Silverlight e importiamo un paio di librerie: Microsoft.Maps.MapControl e Microsoft.Maps.MapControl.Common e inseriamo nella nostra pagina xaml una griglia con all’interno un oggetto di tipo Map.

Ad esempio se vogliamo rappresentare un pushpin bastano queste semplici istruzioni:

   1: Pushpin pushPin = new Pushpin();
   2: pushPin.Tag = p;//oggetto custom che contiene le info da rappresentare
   3: pushPin.Location = new Location(0, 0);
   4: pushPin.Name = p.Address;
   5: pushPin.MouseLeave += new MouseEventHandler(pushPin_MouseLeave);
   6: pushPin.MouseEnter += new MouseEventHandler(pushPin_MouseEnter);
   7: pushPin.MouseLeftButtonDown += new MouseButtonEventHandler(pushPin_MouseLeftButtonDown);
   8: objMap.Children.Add(pushPin); //objMap è la mappa terrestre

Il risultato è il seguente:

Img1

Come potete vedere dal codice ci sono dei gestori sugli eventi del mouse che possiamo utilizzare a nostra scelta ad esempio al MouseLeftButtonDown ho simulato la selezione del pushpin.

   1: private void pushPin_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
   2: {
   3:   if (_handleNavigation)//flag di stato
   4:   {
   5:       _handledPushPinClick = true;
   6:  
   7:       Pushpin p = ((from c in pushPinsList
   8:                     where c == (sender as Pushpin)
   9:                     select c).FirstOrDefault() as Pushpin);
  10:       if (p != null)
  11:       {
  12:           if (selectedPushPin.Contains(p))
  13:           {
  14:               p.Background = new System.Windows.Media.SolidColorBrush(defaultPushPinColor);
  15:               selectedPushPin.Remove(p);//lista dei pushpin selezionati
  16:           }
  17:           else
  18:           {
  19:               p.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Blue);
  20:               selectedPushPin.Add(p);
  21:           }
  22:       }
  23:       
  24:   }
  25: }

Il risultato è il seguente ovvero colorare il pushpin di blu e aggiungere il pushpin ad una lista che sarà quella dei pushpins selezionati:

ImgSelezionato

Una volta gestita la singola selezione vogliamo invece realizzare quella multipla ovvero l’effetto che si ottiene quando ad esempio in una classica applicazione di grafica teniamo premuto il tasto sinistro del mouse trascinando e creando un’area di selezione. Per fare questo dobbiamo prima di tutto disabilitare l’evento di default MousePan della mappa, infatti il click prolungato del tasto sinistro provoca la navigazione ovvero lo spostamento in un area geografica stabilita. Quindi in tale evento si disegna un rettangolo trasparente con i bordi tratteggiati e si valuta la posizione di ogni pushpin.

   1: private void objMap_MousePan(object sender, Microsoft.Maps.MapControl.MapMouseDragEventArgs e)
   2: {
   3:     e.Handled = _handleNavigation;//Prendiamo il controllo dell'evento
   4:     if (_handleNavigation && startDrawSelection)
   5:     {
   6:         if (objMap.Children.Count > 0 && objMap.Children[objMap.Children.Count - 1] is MapPolygon)
   7:             objMap.Children.RemoveAt(objMap.Children.Count - 1);
   8:  
   9:         //Disegno il rettangolo che rappresenta l'area di selezione
  10:         objMap.TryViewportPointToLocation(e.ViewportPoint, out endRect);
  11:         objMap.Children.Add(MapHelper.GetPolygonFromPoint(startRect, endRect));
  12:     }
  13: }

Oltre al MousePan dobbiamo gestire anche MouseLeftButtonDown e MouseLeftButtonUp

   1: private void objMap_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
   2: {
   3:    e.Handled = _handleNavigation; //si prende il controllo dell'evento
   4:  
   5:    if (_handleNavigation)
   6:    {
   7:        startDrawSelection = false;
   8:  
   9:        if (objMap.Children[objMap.Children.Count - 1] is MapPolygon && objMap.Children.Count > 0)
  10:        {
  11:            foreach (Pushpin p in pushPinsList)
  12:            {
  13:                p.Background = new System.Windows.Media.SolidColorBrush(defaultPushPinColor);
  14:            }
  15:  
  16:            selectedPushPin.Clear();
  17:  
  18:            foreach (Pushpin p in pushPinsList)
  19:            {
  20:                LocationRect pushPinRect = new LocationRect(
  21:                                                    new List<Location>(){
  22:                                                p.Location
  23:                                            });
  24:  
  25:                //Creo il rettangolo che sarà l'area di selezione e ne faccio l'intersezione con il pushpin
  26:                LocationRect selRect = new LocationRect((objMap.Children[objMap.Children.Count - 1] as MapPolygon).Locations);
  27:  
  28:                if (selRect.Intersects(pushPinRect))
  29:                {
  30:                    //Coloro di blu il pushpin in quanto è presente nell'area di selezione
  31:                    p.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Blue);
  32:                    selectedPushPin.Add(p);
  33:                }
  34:            }
  35:  
  36:  
  37:            //Nascondo l'area di selezione
  38:            objMap.Children.RemoveAt(objMap.Children.Count - 1);
  39:        }
  40:    }
  41: }

   1: private void objMap_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
   2: {
   3:     e.Handled = _handleNavigation;
   4:     if (_handleNavigation && !_handledPushPinClick)
   5:     {
   6:         objMap.TryViewportPointToLocation(e.GetPosition(sender as Map), out startRect);
   7:         startDrawSelection = true;
   8:  
   9:         foreach (Pushpin p in selectedPushPin)
  10:         {
  11:             p.Background = new System.Windows.Media.SolidColorBrush(defaultPushPinColor);
  12:         }
  13:         selectedPushPin.Clear();
  14:     }
  15:  
  16:     _handledPushPinClick = false;
  17: }

Infine vi posto anche la classe MapHelper che contiene una serie di utility di visualizzazione e disegno sulla mappa.

   1: public class MapHelper
   2: {
   3:  
   4:     /// <summary>
   5:     /// Calculates best map view for an array of points. Similar to VEMap.SetMapView method
   6:     /// </summary>
   7:     /// <param name="points">Array of Location objects</param>
   8:     /// <returns></returns>
   9:     public static BestView CalculateMapView(IList<Location> points, Map objMap)
  10:     {    //Calculate bounding rectangle
  11:         double maxLat = -90, minLat = 90, maxLon = -180, minLon = 180;
  12:         //default zoom scales in km/pixel from http://msdn2.microsoft.com/en-us/library/aa940990.aspx
  13:         double[] defaultScales = { 
  14:                                      78.27152, 
  15:                                      39.13576, 
  16:                                      19.56788, 
  17:                                      9.78394, 
  18:                                      4.89197, 
  19:                                      2.44598, 
  20:                                      1.22299, 
  21:                                      0.61150, 
  22:                                      0.30575, 
  23:                                      0.15287, 
  24:                                      .07644, 
  25:                                      0.03822, 
  26:                                      0.01911, 
  27:                                      0.00955, 
  28:                                      0.00478, 
  29:                                      0.00239, 
  30:                                      0.00119, 
  31:                                      0.0006, 
  32:                                      0.0003 };
  33:  
  34:         //Calculate bounding box for array of locations
  35:         for (int i = 0; i < points.Count; i++)
  36:         {
  37:             if (pointsIdea.Latitude > maxLat)
  38:                 maxLat = pointsIdea.Latitude;
  39:  
  40:             if (pointsIdea.Latitude < minLat)
  41:                 minLat = pointsIdea.Latitude;
  42:  
  43:             if (pointsIdea.Longitude > maxLon)
  44:                 maxLon = pointsIdea.Longitude;
  45:  
  46:             if (pointsIdea.Longitude < minLon)
  47:                 minLon = pointsIdea.Longitude;
  48:         }
  49:  
  50:         //calculate center coordinate of bounding box
  51:         double centerLat = (maxLat + minLat) / 2;
  52:         double centerLong = (maxLon + minLon) / 2;
  53:  
  54:         //create a Location object for the center point            
  55:         Microsoft.Maps.MapControl.Location centerPoint = new Microsoft.Maps.MapControl.Location();
  56:         centerPoint.Latitude = centerLat;
  57:         centerPoint.Longitude = centerLong;
  58:  
  59:         //want to calculate the distance in km along the center latitude between the two longitudes
  60:         double meanDistanceX = HaversineDistance(centerLat, minLon, centerLat, maxLon);
  61:  
  62:         //want to calculate the distance in km along the center longitude between the two latitudes
  63:         double meanDistanceY = HaversineDistance(maxLat, centerLong, minLat, centerLong) * 2;
  64:  
  65:         //calculates the x and y scales
  66:         var meanScaleValueX = meanDistanceX / objMap.Width;
  67:         var meanScaleValueY = meanDistanceY / objMap.Height;
  68:  
  69:         double meanScale;    //gets the largest scale value to work with
  70:         if (meanScaleValueX > meanScaleValueY)
  71:             meanScale = meanScaleValueX;
  72:         else
  73:             meanScale = meanScaleValueY;
  74:  
  75:         //intialize zoom level variable
  76:         int zoom = 1;    //calculate zoom level
  77:         for (var i = 1; i < 19; i++)
  78:         {
  79:             if (meanScale >= defaultScalesIdea)
  80:             {
  81:                 zoom = i;
  82:                 break;
  83:             }
  84:         }
  85:  
  86:         //return a BestView object with the center point and zoom level to use
  87:         return new BestView(centerPoint, zoom);
  88:     }
  89:  
  90:     /// <summary>
  91:     /// Calculate the distance in kilometers between two coordinates
  92:     /// </summary>
  93:     /// <param name="lat1"></param>
  94:     /// <param name="lon1"></param>
  95:     /// <param name="lat2"></param>
  96:     /// <param name="lon2"></param>
  97:     /// <returns></returns>
  98:     private static double HaversineDistance(double lat1, double lon1, double lat2, double lon2)
  99:     {
 100:         double earthRadius = 6371;
 101:         double factor = Math.PI / 180;
 102:         double dLat = (lat2 - lat1) * factor;
 103:         double dLon = (lon2 - lon1) * factor;
 104:         double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + Math.Cos(lat1 * factor) * Math.Cos(lat2 * factor) * Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
 105:         double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); return earthRadius * c;
 106:     }
 107:  
 108:  
 109:     public static MapPolygon GetPolygonFromPoint(Location startPoint, Location endPoint)
 110:     {
 111:         MapPolygon polygon = new MapPolygon();
 112:         //polygon.Fill = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.LightGray);
 113:         polygon.Stroke = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Black);
 114:         polygon.StrokeDashCap = PenLineCap.Square;
 115:         polygon.StrokeDashOffset = 5;
 116:         polygon.StrokeDashArray = new DoubleCollection() { 0, 2.5 };
 117:         polygon.StrokeThickness = 1;
 118:         polygon.Opacity = 1;
 119:         polygon.Locations = new LocationCollection() { 
 120:                 new Location(startPoint), 
 121:                 new Location(endPoint.Latitude, startPoint.Longitude), 
 122:                 new Location(endPoint), 
 123:                 new Location(startPoint.Latitude, endPoint.Longitude) };
 124:  
 125:         return polygon;
 126:     }
 127:  
 128: }
 129:  
 130: public struct BestView
 131: {
 132:     public Location center; public int zoom;    //Constructor
 133:  
 134:     public BestView(Location _center, int _zoom)
 135:     {
 136:         center = _center;
 137:         zoom = _zoom;
 138:     }
 139: }

Alla fine il risultato è il seguente: quando si rilascia il tasto sinistro del mouse il rettangolo tratteggiato sparisce e i 4 pushpins si coloreranno di blu risultando selezionati.

ImgSelezionato

Alla prossima puntata…..

Posted: 20 mar 2010 0:41 da Massimiliano | con no comments
Inserito sotto: , ,
Windows 7 modalità God Mode

Qualche giorno fa girovagando per la rete ho trovato una chicca davvero interessante di Windows 7 che avevo intenzione di condividere con voi. Si tratta della modalità nascosta God Mode. In sostanza è un pannello di controllo “avanzato”. Per accedere alla modalità basta seguire i seguenti passi:

  1. Cliccare con il tasto destro del mouse sul desktop e creare una nuova cartella
  2. Nominare la cartella così : GodMode.{ED7BA470-8E54-465E-825C-99712043E01C}.
  3. Fine…. vi comparirà una cartella con l’icona del pannello di controllo chiamata GodMode

Immagine

Posted: 14 gen 2010 14:21 da Massimiliano | con no comments
Inserito sotto:
DotNetToscana Community Tour : Diario di Bordo

Ciao a tutti, siamo “appena” tornati dal comunity tour organizzato da .NET Toscana e volevo condividere questa piccola pazzia che abbiamo fatto con alcuni membri della comunity nel seguente diario di bordo:

  • 7:00 : Sveglia… mamma mia e chi c’a fà….;
  • 7:30 : Partenza da Cava dei Tirreni.. tempo ottimale…traffico 0….un pò freddo;
  • 7:35 : Fermata distributore Esso (i punti……) pieno e poi via direzione Napoli;
  • 8:05 : Via Ferraris, Napoli, “raccatto” il presidente Michele Aponte e il suo fido scudiero :D l’MVP di cocktail…Marcello………via direzione Roma;
  • 10:05 : Arrivo alla stazione della metro di Roma Anagnina lì c’è Giorgio che ci aspetta e che si è fatto 1 ora di metro per raggiungerci ( era l’unico modo per recuperare un pò di tempo sul tragitto). Ci prendiamo un grande caffè e na chiavica di cornetto offerto dal Presidente con le donazioni anonime a dotnetcampania… si riparte direzione Pisa;
  • 12:45 : Siamo quasi a Firenze… ad un certo punto una fumata nera all’orizzonte ….che è stat che è succies…. un camion ha preso fuoco dall’altra parte della carreggiata mai vista una cosa del genere;
  • 13:15 : Statale Firenze-Pisa-Livorno, ci fermiamo in autogrill, menù doppio x tutti (2 panini 1 coca e 1 caffè 8,20€), espletamento dei bisogni fisici ripartenza per il polo tecnologico di Navacchio Cascina (PI);
  • 14:10 : usciamo a Navacchio Cascina quasi vicino alla meta… interruzione a causa lavori, niente di chè ci affidiamo al mitico tom tom che come al solito ci porta per strade di campagna e ci indica come punto di arrivo 1Km prima della meta; meno male che abbiamo trovato un buon uomo che ci ha semplicemente liquidato: “Il polo tecnologico ? Alla vostra destra”…;
  • 14:20 : Registrazione e consegna del numero per l’estrazione della copia di Windows 7 Ultimate e una licenza di controlli di terze parti;
  • 14:30 : Dopo una presentazione del gruppo DotNetToscana iniziano le sessioni e il Presidente, da buon Presidente “caccia dal sacco” il netbook per il live blogging (è prorio gruossss) inizia Pietro Brambati con la seguente sessione:

    • Sviluppare applicazioni N-Tier con i .NET RIA Services e Silverlight 3.
      E’ la prima volta che ascolto un evento dal vivo, ma in generale mi ha colpito il modo di come Pietro trattasse un argomento del genere in una maniera a dir poco deliziosa, molto curato nei dettagli. Complimenti a lui e anche agli altri speakers. Per i dettagli consultate i post di Michele o il sito della comunity.

  • 15:45 : Inizia la seconda sessione di Marco Minerva fondatore del gruppo DotNetToscana ci parla di:

    • Internet Explorer 8 per gli sviluppatori Web.
      Anche questa sessione è risultata interessante si è trattato in pratica di numerose features che girano intorno a Explorer 8 agli altri browser e ad asp.net. Per i dettagli come sopra.

  • 16:45 : Ultima sessione di Matteo Baglini e Mario Martellini, colleghi di Marco Minerva che ci parlano di :

    • ASP.NET MVC : Clean Web Application.
      Come ci segnala il titolo della sessione, si è parlato del framework MVC messo a disposizione da Microsoft che ci permette di ottenere realmente un disaccoppiamento tra la logica di business e le viste della nostra applicazione, tutto questo da integrare con unit tests “alla cecata”. Consiglio inoltre come detto in un post precedente di seguire i video sull’argomento al sito di BEIT : http://www.microsoft.com/italy/beit/ di Simone Chiaretta;

  • 18:15 : Saluti, domande e estrazione finale… Indovinate chi ha vinto? No, non io, però vi posso dire che in una fila eravamo nell’ordine Marcello, Giorgio, Io , Michele e Massimo Bonanni di DotNetRomaCesta. Alla fine Massimo ha vinto la licenza per i controlli di terze parti e il nostro Giorgio Windows 7 Ultimate…. Io e il Presidente che stavamo in mezzo ai due fuochi abbiamo vinto un pocket coffee offerto da Marcello…. Salutiamo, ringraziamo e invitiamo i presenti al nostro appuntamento di Febbraio e ci rimettiamo in cammino per casa;
  • 19:00 : Inizia a piovere e iniziamo a girare alla cieca per Navacchio al fine di trovare l’autostrada, premetto che il mio cellullare si è scaricato e quindi siamo senza tom tom e allora ci affidiamo all’IGO del Presidente, circa 10 min per trovare i satelliti, alla fine a 500 metri dall’autostrada il navigatore aggancia i satelliti, vabbuò meglio tardi che mai;
  • 19:30 : Rifornimento sempre Esso, solo che era self service e stavamo solo noi, quindi niente punti….;
  • 20:00 : Superiamo Firenze, piove, discussioni sull’evento ma tutto liscio;
  • 20:15 : Passiamo davanti al camion bruciato della mattina, c’è solo uno scheletro di lamiere e i pompieri…..il resto è nulla;
  • 20:30 : Arezzo  5 Km di coda causa lavori…… No comment…… 40 min di traffico…….;
  • 21:10 : Evviva si cammina…..direzione Roma….. riportiamo all’ovile Giorgio che ci offrirà anche la cena a casa sua che intanto si addormenta per prepararsi a cucinare;
  • 23:30 : Arrivo a casa di Giorgio e Antonio (che non è venuto) zona Aurelia, tempo 15 minuti il cuoco ci prepara una pasta fagioli e pancetta da farci leccare i baffi…..spettacolare…. Se mai decidesse di cambiare mestiere ne ha un altro assicurato…. Non è finita salsiccia arrostita e per dessert leccornie provenienti dalla calze ricevute per la Befana. Dopo qualche chiacchiera e risata si è fatto tardi dobbiamo ritornare a casa…;
  • 00:45 : Partenza da Roma direzione Napoli. Michele inizia a “capozziare” dal sonno io mantengo grazie all’aiuto di Marcello che mi racconta in maniera dettagliata come si fanno tutti cocktail del mondo e ci dice che ne ha uno suo segreto che poi ci deve far provare….vedremo in altra occasione…;
  • 02:30 : Finalmente arriviamo a Napoli, svegliamo Michele ci salutiamo e ci diamo appuntamento per la prossima….ripartenza per casa;
  • 03:10 : Arrivo a casa con la musica di Vasco a palla in macchina per non addormentarmi…..quando scendo mi rimbombano ancora le orecchie…..;
  • 3:20 : Distrutto nel letto a 4 di Bastoni…….come dice un mio caro amico: “stò con le pezze”;

    Alla fine un solo aggettivo per definire il tutto : Eccezziunale veramente…….

Live Writer e Code Snippet

Ciao ragazzi volevo condividere un’applicazione corredata di un plug-in utilissima per scrivere post e aggiornare da remoto il proprio blog. Stiamo parlando di Windows Live Writer: in partica un interfaccia  che permette di editare un blog da remoto. Inoltre navigando in rete ho trovato un plug-in per scrivere del codice c#,html,vb etc. all’interno del post stesso utilissimo per la formattazione. L’effetto prodotto è il seguente :

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4:  
   5: public class Test
   6: {
   7:    public Test()
   8:    {
   9:        Products = new List<string>();
  10:    }
  11:  
  12:    public IList<string> Products { get; set; }
  13:    public decimal Price { get; set; }
  14:    public decimal ShipPrice { get; set; }
  15: }

 

Il plug-in lo trovate al seguente link http://lvildosola.blogspot.com/2007/02/code-snippet-plugin-for-windows-live.html.

Ciao ciao

MVC Paging & Sorting Data

Prima di questa estate avevo visto e seguito con interesse i video su MVC di ASP.NET di Simone Chiaretta sul portale Microsoft http://www.microsoft.com/italy/beit;  sono rimasto davvero incuriosito dall’argomento anche perché mi sono sempre chiesto come mai la cara vecchia Microsoft non avesse sviluppato alcun framework a riguardo, cosa che invece nel mondo java è ampiamente in uso e supportato da tempo( ad esempio “Struts di Apache”).

A questo punto mi ero ripromesso, non appena avessi avuto un po’ di tempo ,di provare le funzionalità del framework MVC e di valutare in linea di massima la fattibilità i pro e contro dell’infrastruttura offerta. Ecco, dopo mesi ho iniziato a “smanettare” sul prodotto e in particolare ho iniziato a convertire una parte banale di visualizzazione e data entry di un applicazione web esistente e sviluppata con ASP.NET 2.0. Non penso sia il caso di spiegare come il framework lavora anzi se fate una ricerca in rete vi è molto materiale esaustivo e sicuramente più dettagliato di quello che posso trasmettere io; comunque da una prima impressione quello che ho sicuramente notato è il fatto che purtroppo (o forse meglio) bisogna scrivere un bel po’ di codice html in più cosa che con le asp.net webform classiche non facevamo. Dimentichiamoci infatti gli utilissimi e immediati controlli presenti che trascinavamo dalla toolbox, il ViewState e il Postback etc.. ; infatti, ad esempio, per effettuare il rendering di un html input textbox dovremmo scrivere la seguente linea di codice all'interno della pagina aspx:

<%=Html.TextBox("txtProduct") %>

In realtà la cosa che è un po’ più “scocciante” è legata alla mancanza di controlli che effettuano binding di liste come il DataGridView e  la ListView, comodissimi in quanto permettevano di effettuare sorting e paging in maniera semplice. Fortunatamente ci sono i template che vengono in aiuto creando e “bindando” in automatico le proprietà del nostro modello sotto forma di tabella html, ma non risolvono tutte le classiche problematiche del caso. Ad esempio il sorting e il paging devono essere implementati via codice. Di seguito quindi riporto un possibile punto di partenza per affrontare la problematica.

In particolare ho creato una classe che presa una sorgente come input implementa le logiche di suddivisione delle possibili pagine e che verrà poi utilizzata come modello da visualizzare alla vista in questione.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Linq.Expressions;
   5: using System.Reflection;
   6:  
   7: namespace MVC.MyUtility
   8: {
   9:  
  10:     public class PagedView<T> where T : new()
  11:     {
  12:         private IList<int> _numberList;
  13:         private int _pageSize;
  14:         private int _pageCount;
  15:         private int _totalItemCount;
  16:         private int _pageIndex;
  17:         private bool _isFastNavNextEnabled;
  18:         private bool _isFastNavPrevEnabled;
  19:         private string _sortedProperty;
  20:         private bool _sortedType;
  21:  
  22:         public PagedView(IQueryable<T> source, int pageSize, int index, string sortedProperty, bool sortTypeAscending)
  23:         {
  24:             _numberList = new List<int>();
  25:             _pageSize = pageSize;
  26:             _sortedProperty = sortedProperty;
  27:             _pageIndex = index;
  28:             _sortedType = sortTypeAscending;
  29:             _totalItemCount = source.Count();
  30:             _pageCount = _totalItemCount / _pageSize;
  31:  
  32:             if (_totalItemCount % _pageSize > 0)
  33:                 _pageCount++;
  34:  
  35:             int start = 1;
  36:             int endCycle = pageSize;
  37:  
  38:  
  39:             if (index > 0 && index <= pageSize)
  40:             {
  41:                 //E'il primo blocco di dati
  42:                 start = 1;
  43:                 endCycle = pageSize;
  44:                 _isFastNavNextEnabled = true;
  45:             }
  46:             else
  47:                 if (index % pageSize == 0)
  48:                 {
  49:                     //Si verifica la posizione dell'indice nel range
  50:                     //In questo caso si cambia l'indice
  51:                     start = index;
  52:                     endCycle = index + pageSize;
  53:                     _isFastNavNextEnabled = true;
  54:                     _isFastNavPrevEnabled = true;
  55:                 }
  56:                 else
  57:                 {
  58:                     start = index - (index % pageSize);
  59:                     endCycle = start + pageSize;
  60:                     _isFastNavNextEnabled = true;
  61:                     _isFastNavPrevEnabled = true;
  62:                 }
  63:  
  64:             //Controllo se ho sforato ovvero se 
  65:             //il numero di pagine totali è > di endcycle
  66:             if (endCycle >= PageCount)
  67:             {
  68:                 endCycle = PageCount;
  69:                 _isFastNavNextEnabled = false;
  70:             }
  71:  
  72:  
  73:             for (int i = start; i <= endCycle; i++)
  74:                 _numberList.Add(i);
  75:  
  76:             //if (index >= pageSize)
  77:             //    _isFastNavPrevEnabled = true;
  78:  
  79:             if (sortTypeAscending)
  80:             {
  81:                 if (index > 1)
  82:                     ObjectList = LinqHelper.OrderBy<T>(source, sortedProperty).Skip(pageSize * (index - 1)).Take(pageSize);
  83:                 else
  84:                     ObjectList = LinqHelper.OrderBy<T>(source, sortedProperty).Take(pageSize);
  85:             }
  86:             else
  87:             {
  88:                 if (index > 1)
  89:                     ObjectList = LinqHelper.OrderByDescending<T>(source, sortedProperty).Skip(pageSize * (index - 1)).Take(pageSize);
  90:                 else
  91:                     ObjectList = LinqHelper.OrderByDescending<T>(source, sortedProperty).Take(pageSize);
  92:             }
  93:  
  94:         }
  95:  
  96:         public IQueryable<T> ObjectList
  97:         {
  98:             get;
  99:             set;
 100:         }
 101:  
 102:         public IList<int> PageList
 103:         {
 104:             get
 105:             {
 106:                 return _numberList;
 107:             }
 108:         }
 109:  
 110:         public int PageSize
 111:         {
 112:             get
 113:             {
 114:                 return _pageSize;
 115:             }
 116:         }
 117:  
 118:         public int PageCount
 119:         {
 120:             get
 121:             {
 122:                 return _pageCount;
 123:             }
 124:         }
 125:  
 126:         public int PageIndex
 127:         {
 128:             get
 129:             {
 130:                 return _pageIndex;
 131:             }
 132:         }
 133:  
 134:         public bool IsNextAvailable
 135:         {
 136:             get
 137:             {
 138:                 return ((_pageIndex * _pageSize) < _totalItemCount);
 139:             }
 140:         }
 141:  
 142:         public bool IsPrevAvailable
 143:         {
 144:             get
 145:             {
 146:                 return !(_pageIndex == 1 && ObjectList.Count() > 0);
 147:             }
 148:         }
 149:  
 150:         public bool IsFastNavNextAvailable
 151:         {
 152:             get
 153:             {
 154:                 return _isFastNavNextEnabled;
 155:             }
 156:         }
 157:  
 158:         public bool IsFastNavPrevAvailable
 159:         {
 160:             get
 161:             {
 162:                 return _isFastNavPrevEnabled;
 163:             }
 164:         }
 165:  
 166:         public string SortedProperty
 167:         {
 168:             get
 169:             {
 170:                 return _sortedProperty;
 171:             }
 172:         }
 173:  
 174:         public bool IsSortTypeAscending
 175:         {
 176:             get
 177:             {
 178:                 return _sortedType;
 179:             }
 180:         }
 181:  
 182:     }
 183:  
 184:     public static class LinqHelper
 185:     {
 186:         public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string property)
 187:         {
 188:             return ApplyOrder<T>(source, property, "OrderBy");
 189:         }
 190:         public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string property)
 191:         {
 192:             return ApplyOrder<T>(source, property, "OrderByDescending");
 193:         }
 194:         public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string property)
 195:         {
 196:             return ApplyOrder<T>(source, property, "ThenBy");
 197:         }
 198:         public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, string property)
 199:         {
 200:             return ApplyOrder<T>(source, property, "ThenByDescending");
 201:         }
 202:  
 203:         private static IOrderedQueryable<T> ApplyOrder<T>(IQueryable<T> source, string property, string methodName)
 204:         {
 205:             string[] props = property.Split('.');
 206:             Type type = typeof(T);
 207:             ParameterExpression arg = Expression.Parameter(type, "x");
 208:             Expression expr = arg;
 209:             foreach (string prop in props)
 210:             {
 211:                 // use reflection (not ComponentModel) to mirror LINQ            
 212:                 PropertyInfo pi = type.GetProperty(prop);
 213:                 expr = Expression.Property(expr, pi);
 214:                 type = pi.PropertyType;
 215:             }
 216:             Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
 217:             LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);
 218:             object result =
 219:                 typeof(Queryable).GetMethods()
 220:                 .Single(method => method.Name == methodName &&
 221:                                   method.IsGenericMethodDefinition &&
 222:                                   method.GetGenericArguments().Length == 2 &&
 223:                                   method.GetParameters().Length == 2).
 224:                 MakeGenericMethod(typeof(T), type).Invoke(null, new object[] { source, lambda });
 225:             return (IOrderedQueryable<T>)result;
 226:         }
 227:     }
 228: }

 Il tipo T non è altro che il tipo dell’oggetto da “bindare” nella nostra griglia, nel mio caso come modello dati ho utilizzato Entity Framework per cui nell’esempio e un tipo “Product”. Il costruttore ha come input i seguenti parametri :

IQueryable<T> source //sorgente dati 
int pageSize //Dimensione della pagina ovvero numero di righe della griglia 
int index //Indice della Pagina corrente
string sortedProperty //Proprietà dell’oggetto di tipo T su cui effettuare il sorting
bool sortTypeAscending //true se il sorting è Ascending

L’oggetto infine espone le seguenti proprietà:

IList<int> PageList //List dei numeri di pagina da linkare per il paginig
int PageSize //Dimensione della pagine ovvero numero di item da mostrare
int PageCount //Dimensione totale delle pagine
int PageIndex //Indice della pagina corrente
bool IsNextAvailable //Flag che indica se esiste una pagina successiva
bool IsPrevAvailable //Flag che indica se esiste una pagina precedente
bool IsFastNavNextAvailable //Flag che indica se è esiste una pagina successiva al valore di PageIndex + PageSize
bool IsFastNavPrevAvailable//Flag che indica se è esiste una pagina precedente  al valore di PageIndex – PageSize
string SortedProperty //Proprietà dell’oggetto di tipo T su cui effettuare il sorting
bool IsSortTypeAscending //true se il sorting è Ascending
IQueryable<T> ObjectList //lista degli oggetti manipolata in base ai parametri del costruttore

 Quindi la Action del nostro Controller non fa altro che istanziare un oggetto di tipo PagedView<Product> e passarlo alla vista .

   1: public ActionResult Index()
   2: {
   3:     PagedView<Product> obj = new PagedView<Product>(GetProductList(),_nrItemPage, 1, "CodProduct", true);
   4:     return View(obj);
   5: }

 A questo punto manca la creazione della vista che sarà di tipo strongly-typed e che effettuerà il render dell’oggetto di tipo PagedView<Product>.

   1: <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MVC.MyUtility.PagedView<WebSellingMVC.Models.Product>>" %>
   2:  
   3: <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
   4:     Product List
   5: </asp:Content>
   6: <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
   7:  
   8:     <script src="/Scripts/MicrosoftAjax.js" type="text/javascript" />
   9:     <script src="/Scripts/jquery-1.3.2.js" type="text/javascript"/>
  10:     <script src="/Scripts/MicrosoftMvcAjax.js" type="text/javascript"/>
  11:  
  12:       <h2>Product List</h2>
  13:       <br />
  14:       <%Html.RenderPartial("ProductCtrl", Model); %>
  15: </asp:Content>

Come avrete potuto notare sono stati aggiunti dei riferimenti a javascript  (sono inclusi automaticamente dal template della soluzione MVC)  e ad uno User Control; la motivazione è semplice , vogliamo implementare sia il sorting che il paging attraverso l’utilizzo di Ajax. Big Smile Quindi la View index.aspx non fa altro che da contenitore al controllo ProductCtrl.ascx ( il seguente blocco di codice) il quale realizza graficamente la griglia degli oggetti Product e che viene popolata attraverso richieste Ajax.

   1: <%@ Control Language="C#" 
   2: Inherits="System.Web.Mvc.ViewUserControl<MVC.MyUtility.PagedView<WebSellingMVC.Models.Product>>" %>
   3:  
   4: <div id="sortContainer">
   5:     <table width="100%">
   6:         <tr>
   7:             <td colspan="10">
   8:                 <% if (Model.IsFastNavPrevAvailable)
   9:                    {%>
  10:                 <%= Ajax.ActionLink("<< ", "MovePrevFast", new { pageIndex = Model.PageIndex, propertyToSort = Model.SortedProperty, sortType = Model.IsSortTypeAscending }, new AjaxOptions()
  11: {
  12:     UpdateTargetId = "sortContainer",
  13:     InsertionMode = InsertionMode.Replace    
  14: })%>
  15:                 <% }%>
  16:                 <% foreach (int item in Model.PageList)
  17:                    { %>
  18:                 <%= Ajax.ActionLink(item.ToString() + " ", "MovePage", new { pageIndex = item, propertyToSort = Model.SortedProperty, sortType = Model.IsSortTypeAscending }, new AjaxOptions()
  19: {
  20:     UpdateTargetId = "sortContainer",
  21:     InsertionMode = InsertionMode.Replace
  22: })%>
  23:                 <% }%>
  24:                 <% if (Model.IsFastNavNextAvailable)
  25:                    {%>
  26:                 <%= Ajax.ActionLink(">> ", "MoveNextFast", new { pageIndex = Model.PageIndex, propertyToSort = Model.SortedProperty, sortType = Model.IsSortTypeAscending }, new AjaxOptions()
  27: {
  28:     UpdateTargetId = "sortContainer",
  29:     InsertionMode = InsertionMode.Replace
  30: })%>
  31:                 <% }%>
  32:             </td>
  33:         </tr>
  34:         <tr>
  35:             <th>
  36:                 <%= Ajax.ActionLink("CodProduct", "Sort", new { pageIndex = Model.PageIndex, propertyToSort = "CodProduct", sortType = !Model.IsSortTypeAscending }, new AjaxOptions()
  37: {
  38:     UpdateTargetId = "sortContainer",
  39:     InsertionMode = InsertionMode.Replace
  40: })%>
  41:             </th>
  42:             <th>
  43:                 <%= Ajax.ActionLink("NameIT", "Sort", new { pageIndex = Model.PageIndex, propertyToSort = "NameIT", sortType = !Model.IsSortTypeAscending }, new AjaxOptions()
  44: {
  45:     UpdateTargetId = "sortContainer",
  46:     InsertionMode = InsertionMode.Replace
  47: })%>
  48:             </th>
  49:             <th>
  50:                 <%= Ajax.ActionLink("NameEN", "Sort", new { pageIndex = Model.PageIndex, propertyToSort = "NameEN", sortType = !Model.IsSortTypeAscending }, new AjaxOptions()
  51: {
  52:     UpdateTargetId = "sortContainer",
  53:     InsertionMode = InsertionMode.Replace
  54: })%>
  55:             </th>
  56:             <th>
  57:                 <%= Ajax.ActionLink("CategoryIT", "Sort", new { pageIndex = Model.PageIndex, propertyToSort = "CategoryIT", sortType = !Model.IsSortTypeAscending }, new AjaxOptions()
  58: {
  59:     UpdateTargetId = "sortContainer",
  60:     InsertionMode = InsertionMode.Replace
  61: })%>
  62:             </th>
  63:             <th>
  64:                 <%= Ajax.ActionLink("CategoryEN", "Sort", new { pageIndex = Model.PageIndex, propertyToSort = "CategoryEN", sortType = !Model.IsSortTypeAscending }, new AjaxOptions()
  65: {
  66:     UpdateTargetId = "sortContainer",
  67:     InsertionMode = InsertionMode.Replace
  68: })%>
  69:             </th>
  70:             <th>
  71:                 <%= Ajax.ActionLink("TeamLine", "Sort", new { pageIndex = Model.PageIndex, propertyToSort = "TeamLine", sortType = !Model.IsSortTypeAscending }, new AjaxOptions()
  72: {
  73:     UpdateTargetId = "sortContainer",
  74:     InsertionMode = InsertionMode.Replace
  75: })%>
  76:             </th>
  77:             <th>
  78:                 <%= Ajax.ActionLink("Brand", "Sort", new { pageIndex = Model.PageIndex, propertyToSort = "Brand", sortType = !Model.IsSortTypeAscending }, new AjaxOptions()
  79: {
  80:     UpdateTargetId = "sortContainer",
  81:     InsertionMode = InsertionMode.Replace
  82: })%>
  83:             </th>
  84:             <th>
  85:                 <%= Ajax.ActionLink("UM", "Sort", new { pageIndex = Model.PageIndex, propertyToSort = "UM", sortType = !Model.IsSortTypeAscending }, new AjaxOptions()
  86: {
  87:     UpdateTargetId = "sortContainer",
  88:     InsertionMode = InsertionMode.Replace
  89: })%>
  90:             </th>
  91:             <th>
  92:                 <%= Ajax.ActionLink("Quantity", "Sort", new { pageIndex = Model.PageIndex, propertyToSort = "Quantity", sortType = !Model.IsSortTypeAscending }, new AjaxOptions()
  93: {
  94:     UpdateTargetId = "sortContainer",
  95:     InsertionMode = InsertionMode.Replace
  96: })%>
  97:             </th>
  98:             <th>
  99:                 <%= Ajax.ActionLink("Price", "Sort", new { pageIndex = Model.PageIndex, propertyToSort = "Price", sortType = !Model.IsSortTypeAscending }, new AjaxOptions()
 100: {
 101:     UpdateTargetId = "sortContainer",
 102:     InsertionMode = InsertionMode.Replace
 103: })%>
 104:             </th>
 105:         </tr>
 106:         <% foreach (var item in Model.ObjectList)
 107:            {%>
 108:         <tr>
 109:             <td>
 110:                 <%= Html.Encode(item.CodProduct) %>
 111:             </td>
 112:             <td>
 113:                 <%= Html.Encode(item.NameIT) %>
 114:             </td>
 115:             <td>
 116:                 <%= Html.Encode(item.NameEN) %>
 117:             </td>
 118:             <td>
 119:                 <%= Html.Encode(item.CategoryIT) %>
 120:             </td>
 121:             <td>
 122:                 <%= Html.Encode(item.CategoryEN) %>
 123:             </td>
 124:             <td>
 125:                 <%= Html.Encode(item.TeamLine) %>
 126:             </td>
 127:             <td>
 128:                 <%= Html.Encode(item.Brand) %>
 129:             </td>
 130:             <td>
 131:                 <%= Html.Encode(item.UM) %>
 132:             </td>
 133:             <td>
 134:                 <%= Html.Encode(item.Quantity) %>
 135:             </td>
 136:             <td>
 137:                 <%= Html.Encode(String.Format("{0:F}", item.Price)) %>
 138:             </td>
 139:         </tr>
 140:         <% }%>
 141:         <tr>
 142:             <td colspan="8">
 143:                 Page
 144:                 <%= Html.Encode(Model.PageIndex) %>
 145:                 of
 146:                 <%= Html.Encode(Model.PageCount) %>
 147:             </td>
 148:             <td colspan="2" align="right">               
 149:                 <%if (Model.IsPrevAvailable)
 150:                   { %>
 151:                 <%= Ajax.ActionLink("Prev", "MovePage", new { pageIndex = Model.PageIndex - 1, propertyToSort = Model.SortedProperty, sortType = Model.IsSortTypeAscending }, new AjaxOptions()
 152: {
 153:     UpdateTargetId = "sortContainer",
 154:     InsertionMode = InsertionMode.Replace
 155: })%>
 156:                 <% }%>
 157:                  <%if (Model.IsNextAvailable)
 158:                   { %>
 159:                 <%= Ajax.ActionLink("Next", "MovePage", new { pageIndex = Model.PageIndex + 1, propertyToSort = Model.SortedProperty, sortType = Model.IsSortTypeAscending }, new AjaxOptions()
 160: {
 161:     UpdateTargetId = "sortContainer",
 162:     InsertionMode = InsertionMode.Replace
 163: })%>
 164:                 <% }%>
 165:             </td>
 166:         </tr>
 167:     </table>
 168: </div>

 Nella prima riga  renderizziamo i pulsanti che realizzano la navigazione delle pagine in base alla dimensione della lista sorgente. Infatti con la seguente istruzione valutiamo se è abilitata la navigazione veloce ovvero spostiamo l’indice di più pagine alla volta, in caso affermativo viene creato il codice html per invocare una Action via Ajax del Controller in questione denominata "MovePrevFast" passando come parametri :

· l’indice della pagina attuale;

· la proprietà dell’oggetto Product su cui effettuare sorting;

· se il sorting è di tipo Ascending.

Inoltre con l’ultimo parametro indichiamo che l’obiettivo della chiamata è il container con id=”sortContainer” e che i dati ricevuti dalla chiamata Ajax dovranno sostituire il contenuto di tale container.

   1: <% if (Model.IsFastNavPrevAvailable) {%>
   2: <%= Ajax.ActionLink("<< ", 
   3:     new { 
   4:         PageIndex, 
   5:         propertyToSort = Model.SortedProperty, 
   6:         sortType = Model.IsSortTypeAscending 
   7:         },
   8:     new AjaxOptions()
   9:         {
  10:             Id = "sortContainer",
  11:             e = InsertionMode.Replace    
  12:         })%>
  13: <% }%>

 Tutto questo lato Controller si sviluppa così :

   1: public ActionResult MovePrevFast(int pageIndex, string propertyToSort, bool sortType)
   2: {
   3:    PagedView<Product> obj = new PagedView<Product>(GetProductList(), _nrItemPage, pageIndex - _nrItemPage, propertyToSort, sortType);
   4:  
   5:    if (!Request.IsAjaxRequest())
   6:        return View("Index", obj);
   7:    else
   8:        return PartialView("ProductCtrl", obj);
   9: }

Ovvero si crea l’oggetto PagedView<Product> , poi nell’eventualità che la richiesta sia di tipo Ajax come nel nostro caso allora si passa tale oggetto come PartialView cioè allo User Control "ProductCtrl", altrimenti se ad esempio il browser non supporta richieste Ajax o i javascript sono disabilitati ci comportiamo come se stessimo effettuando una normale richiesta http renderizzando tutta la view come accade per la prima volta che viene effettuata la richiesta della pagina stessa. Un discorso simile è fatto sugli action link numerici che indicano l'indice delle pagine, con la differenza che l'action invocata è "MovePage" che mouve l'indice della pagina in base alla scelta dell'utente.

Allo stesso modo implementiamo il sorting; infatti nello User Control subito dopo la riga che contiene la navigazione vi è l’intestazione delle colonne della griglia che sono degli Action Link per scatenare appunto l’evento di sorting. Come per il precedente caso la seguente istruzione genera il codice html che permette di effettuare richieste via Ajax alla Action "Sort" del Controller Product passando gli stessi parametri del caso precedente e comportandosi nello stesso modo.

   1: <%= Ajax.ActionLink(
   2:         "CodProduct",
   3:         "Sort",
   4:          new { 
   5:             pageIndex = Model.PageIndex, 
   6:             propertyToSort = "CodProduct", 
   7:             sortType = !Model.IsSortTypeAscending 
   8:             }, 
   9:             new AjaxOptions()
  10:             {
  11:                 UpdateTargetId = "sortContainer",
  12:                 InsertionMode = InsertionMode.Replace
  13:             })%>

Lato Controller invece implementiamo la Action in questione nel seguente modo simile al caso precedente:

   1: public ActionResult Sort(int pageIndex, string propertyToSort, bool sortType)
   2: {
   3:         PagedView<Product> obj = new PagedView<Product>(
   4:                                             GetProductList(),
   5:                                             _nrItemPage, 
   6:                                             pageIndex, 
   7:                                             propertyToSort, 
   8:                                             sortType);
   9:                                             
  10:         if (!Request.IsAjaxRequest())
  11:             return View("Index", obj);
  12:         else
  13:             return PartialView("ProductCtrl", obj);
  14: }

Tutto si traduce nel seguente risultato che è ampiamente personalizzabile:

Result

Naturalmente questo è solo un punto d’inizio il tutto può essere adattato e migliorato i base alle vostre esigenze, ad esempio al posto di una PartialView potremmo restituire un JSON result formattando quindi i dati. A voi la scelta…..

See you soon…

Posted: 13 nov 2009 15:56 da Massimiliano | con no comments
Inserito sotto: , ,
Benvenuti e Grazie !!!

Ciao Ragazzi, questo è il mio primo post in assoluto (che emozione..... Big Smile); volevo quindi rendervi partecipi di questa mia nuova avventura. Cercherò (almeno spero) di essere di aiuto a tutta la comunity dell'ambiente .NET. In particolar modo però volevo ringraziare il mitico Michele Aponte che mi ha concesso l'onore di questo blog e che si impegna insieme ad altri ragazzi nella divulgazione di informazioni interessanti e nella manutenzione del "nostro portale campano" http://dotnetcampania.org/. Complimenti e nuovamente grazie.

A Presto

Massimiliano

Posted: 4 nov 2009 15:11 da Massimiliano | con 2 comment(s)
Inserito sotto: