.NET: effettuare query SQL su DataTable in memoria

2 02 2009

Scenario: realizzazione di un sistema di ‘aggregazione dati’ in grado di caricare in memoria dati da fonti diverse (Db SQLServer/Oracle/MySQL, files Excel, Db Access) ed utilizzarli per realizzare semplici ‘reports’.

Riempire di dati le DataTables e’ cosa da poco, aggregarle all’interno di un DataSet e’ ancora piu’ semplice, comincia a saltar fuori qualche difficolta’ quando arriva il momento di permettere agli utenti di  ‘interrogare’ il DataSet utilizzando una sintassi ‘SQL-Like’: soluzione ottima per l’utente finale con una minima infarinatura di Structured Query Language, difficile da realizzare senza scomodare LINQ e il Framework 3.5.

Per fortuna ci viene incontro Emmet Gray, ottimo programmatore che ha realizzato una classe che implementa un engine SQL minimale utilizzabile sui DataSet: SQL_Engine.

L’utilizzo di tale classe e’ straordinariamente semplice: una volta riempito il nostro DataSet con le tabelle contenenti i dati da interrogare, basta instanziare l’oggetto passando al constructor il dataset


Dim sc As New SQL_Engine.SelectCommand(ds)

e richiamare il metodo ‘Execute’ (passando come parametro la query da eseguire) , che si occupera di restituire una DataTable contenente i risultati


Dim dt As DataTable = sc.Execute(query)




VB.NET: inserire un post su BlogSpot utilizzando le Google Blogger Data API

20 10 2008

Un breve frammento di codice che sto utilizzando per la realizzazione di un tool di ‘blog-sync’ (che rilascero’ a breve).

Utilizzando le Google Blogger Data API e’ possibile collegarsi alla piattaforma di blogging di Google e aggiungere un post a un blog.

Per prima cosa e’ necessario scaricare le Google Data API e, una volta scompattato l’archivio, e’ necessario aggiungere nel progetto VB le reference ai file presenti nella directory ‘Redist’.

A questo punto, realizziamo una breve sub in VB:


Imports Google.GData.Extensions
Imports Google.GData.GoogleBase
Imports Google.GData.Client
Imports System.Net
Imports System.Xml
Imports System.IO
Imports System.Text.RegularExpressions

  Private Sub PostToBlogger(ByVal titolo As String, ByVal contenuto As String,  ByVal blogid As String)
        Dim service As Service = New Service("blogger", "BlogName")
        service.Credentials = New GDataCredentials("BLOGGER USERNAME", "BLOGGER PASSWORD")
        Dim factory As GDataGAuthRequestFactory = service.RequestFactory()
        factory.AccountType = "GOOGLE"

        Dim newPost As New AtomEntry()
        newPost.Title.Text = titolo
        newPost.Content = New AtomContent()
        newPost.Content.Content =  contenuto
        TextBox1.Text = TextBox1.Text & "--- " & newPost.Title.Text
        newPost.Updated = Date.Now

        Dim query As New FeedQuery()
        query.Uri = New Uri("http://www.blogger.com/feeds/" + blogid + "/posts/default")

        ' Verifico se il post e' gia' presente. Se si esco.
        Dim feed As AtomFeed = service.Query(query)
        For Each entry As AtomEntry In feed.Entries
            If Trim(entry.Title.Text).ToUpper = Trim(newPost.Title.Text).ToUpper Then
                TextBox1.Text = TextBox1.Text & " -- NOTIZIA GIA' PRESENTE --" & vbCrLf
                Exit Sub
            End If
        Next

        Dim blogFeedUri As New Uri("http://www.blogger.com/feeds/" + blogid + "/posts/default")
        Try
            Dim createdEntry As AtomEntry = service.Insert(blogFeedUri, newPost)
            TextBox1.Text = TextBox1.Text & " -- PUBBLICATO --" & vbCrLf
        Catch ex As Exception
            TextBox1.Text = TextBox1.Text & " -- ERRORE: " & vbCrLf & ex.InnerException.Message & vbCrLf
        End Try
    End Sub

 

La sub si discosta da quanto presentato nella documentazione ufficiale solo per la possibilita’ di verificare che il post sia stato gia’ inserito (proprio per la necessita’ di utilizzare in un tool di sincronizzazione di blogs) prendendo come ‘dato univoco’ il titolo dello stesso.

Probabilmente il metodo di identificazione dei duplicati e’ migliorabile, ci lavorero’! (naturalmente ogni suggertimento e’ ben accetto!).





VB.NET: rilevare un server di Quake3 su una rete locale

10 10 2008

Classica applicazione inutile ma divertente da sviluppare :-)

Qualche giorno fa ho iniziato a chiedermi come il mio amato Quake3 Arena lavorasse per ricercare i server per il gioco in multiplayer sulla rete locale.

Dopo una breve ricerca su Google mi sono imbattuto in questo articolo che spiega a grandi linee come funziona il protocollo utilizzato dal motore di Q3A: tutto basato su UDP (scelta obbligata, per la maggiore velocità) e a dire il vero abbastanza semplice e lineare.

To query a server is very simple. Send a connectionless (UDP) packet with 4 OOB header bytes (0xff) and the text string getstatus. There are many sites which contain a thorough description of this so I won’t go into details.

Quindi, per rilevare se su una macchina remota sia attivo un server è sufficiente forgiare un pacchetto UDP come descritto sopra, inviarlo sulla porta 27960 e attendere una eventuale risposta (non all’infinito, essendo UDP un protocollo ‘connection-less’).

Con queste informazioni la realizzazione di una semplice funzione in VB.NET è cosa da poco:


Imports System.Net

Public Function CheckServer(ByVal hostaddress As String)
        Dim _UdpClient As New System.Net.Sockets.UdpClient

        Dim client As New Sockets.Socket(Sockets.AddressFamily.InterNetwork, Sockets.SocketType.Dgram, Sockets.ProtocolType.Udp)

        client.ReceiveTimeout = 5
        client.Connect(IPAddress.Parse(hostaddress), 27960)

        Dim bytCommand As Byte() = System.Text.Encoding.ASCII.GetBytes("xxxxxgetstatus")
        bytCommand(0) = Byte.Parse("255")
        bytCommand(1) = Byte.Parse("255")
        bytCommand(2) = Byte.Parse("255")
        bytCommand(3) = Byte.Parse("255")
        bytCommand(4) = Byte.Parse("02")

        Dim remoteEndPoint As New IPEndPoint(IPAddress.Any, 0)

        Dim pret As String = client.Send(bytCommand, socketFlags:=Sockets.SocketFlags.None)
        Dim bufferRec(65000) As Byte
        Try
            client.Receive(bufferRec)
            Return System.Text.Encoding.ASCII.GetString(bufferRec)

        Catch ex As Exception
            Return ""
        End Try

    End Function

Da notare che ho settato manualmente il timeout della connessione e ‘trappato’ l’errore di connessione, verificando in questo modo se il server sia in funzione o meno.

La funzione mi restituisce una stringa contenente (qualora sia attivo un server sulla macchina esaminata) una serie di informazioni sulla partita in corso; nel caso la connessione vada in timeout, restituisce una stringa vuota.

Ora so verificare se su un determinato sistema stia girando Q3A in modalità multiplayer, il passo successivo è ripetere questa procedura per tutti quelli presenti sulla mia rete locale.

Per farlo questo mi sono affidato a una soluzione ’sporca’ ma funzionale: utilizzo il comando ‘net view’ di windows, ne analizzo il risultato ottenendo un elenco di indirizzi:


Public Function GetIpAddresses()
        Dim addresses As New ArrayList
        Dim MyAdd As String = Dns.GetHostByName(Dns.GetHostName()).AddressList(0).ToString
        Dim psi As System.Diagnostics.ProcessStartInfo = New System.Diagnostics.ProcessStartInfo()
        psi.FileName = ("C:\WINDOWS\System32\cmd.exe")
        psi.Arguments = "/c net view > lista.txt"
        psi.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
        Application.DoEvents()
        System.Diagnostics.Process.Start(psi)
        Dim sr As System.IO.StreamReader = Nothing
        Dim run As Boolean = False
        While run = False
            Application.DoEvents()
            Try
                System.Threading.Thread.Sleep(1000)
                sr = New System.IO.StreamReader(Application.StartupPath & "\lista.txt")
                run = True

            Catch ex As Exception
                run = False
            End Try
        End While

        While sr.ReadLine().StartsWith("--")  True
            Application.DoEvents()
        End While

        Dim str As String = ""
        Dim comp(64) As String
        Dim i As Integer = 0

        While str.StartsWith("The")  True
            Application.DoEvents()
            str = sr.ReadLine
            comp(i) = str.Split(Char.Parse(" "))(0)
            comp(i) = comp(i).Substring(2, comp(i).Length - 2)
            If comp(i) = "e" Then
                Application.DoEvents()
                comp(i) = Nothing
            End If
            i = i + 1
        End While
        sr.Close()
        sr = Nothing
        For Each s As String In comp
            If s  Nothing Then
                If s.ToUpper  Dns.GetHostName.ToUpper Then
                   addresses.Add(Dns.GetHostByName(s).AddressList(0).ToString)
                End If
            End If
        Next
        Return addresses
    End Function

a questo punto non mi resta che sottoporre l’array di indirizzi restituito dalla funzione GetIpAddresses a CheckServer e, se viene rilevato un server, leggere i dati restituiti e formattarli adeguatamente:


 Dim lista As ArrayList = GetIpAddresses()        

        For Each riga As String In lista.ToArray
            Dim server As String = CheckServer(riga)
            If server  "" Then
                Dim ServerName As String = server.Split("\")(Array.IndexOf(server.Split("\"), "sv_hostname") + 1)
                Dim MapName As String = server.Split("\")(Array.IndexOf(server.Split("\"), "mapname") + 1)
                Dim FragLimit As String = server.Split("\")(Array.IndexOf(server.Split("\"), "fraglimit") + 1)
                Dim TimeLimit As String = server.Split("\")(Array.IndexOf(server.Split("\"), "timelimit") + 1)
                Dim Version As String = server.Split("\")(Array.IndexOf(server.Split("\"), "version") + 1)

                risultato = risultato & "--- Trovato Server ---" & vbCrLf
                risultato = risultato & "Ip Address:" & riga & vbCrLf
                risultato = risultato & "Server Name: " & ServerName & vbCrLf
                risultato = risultato & "Mappa: " & MapName & vbCrLf
                risultato = risultato & "FragLimit: " & FragLimit & vbCrLf
                risultato = risultato & "TimeLimit: " & TimeLimit & FragLimit & vbCrLf
                risultato = risultato & "Versione: " & Version & FragLimit & vbCrLf

                TextBox1.Text =  TextBox1.Text  & risultato
            End If
            Application.DoEvents()
        Next   
    End Sub

Ho completato il tutto con il codice necessario a ridurre l’applicazione nella system tray (facendo ripetere la procedura di scan ogni 5 secondi) e a far riapparire un form qualora la ricerca fornisca esito positivo.

Il sorgente completo è scaricabile da QUI.

(e sperate che non finisca mai tra le mani del vostro capoufficio!) :-D





.NET: gestione manuale della Garbage Collection

26 09 2008

Per chi arriva da linguaggi di ‘basso livello’, il Garbage Collector è una scoperta allo stesso tempo meravigliosa e sconvolgente, che (cito Wikipedia) porta ad un notevole cambio nello stile di programmazione dei linguaggi che lo implementano. Infatti non è più possibile richiedere esplicitamente di liberare la memoria utilizzata da un oggetto, ovvero terminare tale oggetto in modo deterministico, ma si lascia che il sistema esegua questa operazione automaticamente, nel momento in cui lo riterrà più opportuno per migliorare le prestazioni complessive (finalizzazione non deterministica).

Non quindi possibile, con un linguaggio ad ‘alto livello’, finalizzare un oggetto singolarmente ma è necessario porlo nella ‘raccolta di rifiuti’ e attendere che il sistema/compilatore si occupi di fare pulizia.

Può tuttavia essere necessario ‘forzare’ questa procedura di pulizia, anzichè lasciarla gestire automaticamente: un esempio potrebbe essere l’utilizzo di oggetti che incapsulano molte ‘risorse non gestite’ (ad esempio risorse del sistema operativo, quali file, finestre o connessioni di rete), quando l’applicazione non necessità più dell’oggetto, è certo che le risorse da esso utilizzate non saranno più necessarie. Ai fini delle prestazioni, può essere opportuno rilasciarle tutte insieme.

Per eseguire il ‘collect’ della GC, il Framework .NET fornisce, nella classe GC (Garbage Collection) il metodo GC.Collect, che (da notare bene) prima di eseguire una procedura di Garbage Collection sospende tutti i thread in esecuzione.

Tale funzionamento comporta infatti una riduzione delle prestazioni che può diventare problematica se si chiama GC.Collect più spesso del necessario. È opportuno non inserire codice che richiama GC.Collect in quei punti del programma a cui l’utente accede frequentemente. Tale pratica potrebbe infatti impedire al motore di ottimizzazione di determinare il momento migliore in cui eseguire una procedura di Garbage Collection, facendo decadere in maniera drastica le prestazioni dell’applicazione.





VB.NET Tips: calcolare lo UnixTime

22 04 2008

Attualmente al framework .NET manca una cosa abbastanza utile: non esiste una funzione che restituisca lo unixtime attuale.

Lo UnixTime, vi ricordo, e’ l’unita’ di misura del tempo utilizzata nei sistemi UnixLike e rappresenta il numero di secondi che sono trascorsi dalla mezzanotte (UTC) dell’1 gennaio 1970.

Questo breve code snippet permette di calcolare tale valore:


unixtime  = (DateTime.UtcNow - New DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds




VB.NET: creare una tabella su DataBase partendo da un dataset

1 04 2008

Nel precedente post ho spiegato come popolare un dataset partendo da un file di testo. Ora facciamo un passo successivo: portiamo il dataset creato su un database MSSQL.

Il dataset creato con la funzione del post precedente ha il vantaggio di riconoscere nella maggior parte dei casi i tipi di dati contenuti nel file di testo importato, permettendoci quindi di scorrerne le colonne e creare sul database una tabella adeguata a contenerne i dati.

Il nome della tabella verra’ ottenuto dalla proprieta’ DataSetName dell’oggetto dataset.

La connessione al database dovra essere infine effettuata con delle credenziali con la possibilita’ di creare e modificare tabelle.

Public Function DataSet2SQLTable(ByVal ds As DataSet)

Dim cn As SqlConnection = New SqlConnection(My.Settings.ConnectionString1.ToString)

cn.Open()
Dim cmd As SqlCommand = New SqlCommand(“CREATE TABLE [" & ds.DataSetName & "] ([id] [int] IDENTITY (1, 1) NOT NULL) ON [PRIMARY]“, cn)
cmd.ExecuteNonQuery()
cn.Close()

Dim colonna As DataColumn = Nothing

For Each colonna In ds.Tables(0).Columns
Dim nome_colonna = colonna.ColumnName
Dim tipo_colonna = SysType2DBType(colonna.DataType)
Dim tipo_colonna_str As String = tipo_colonna.ToString

Dim dimensioni As String = “”

If tipo_colonna_str.ToUpper = “NVARCHAR” Then
dimensioni = “(250)”
End If

Dim str_query = “ALTER TABLE [" & ds.DataSetName & "] ADD [" & nome_colonna.ToString.Replace(ds.DataSetName.Replace(".", "#"), "") & "] ” & tipo_colonna_str & ” ” & dimensioni & “NULL”

cn.Open()
cmd = New SqlCommand(str_query, cn)
cmd.ExecuteNonQuery()
cn.Close()

Next

‘importazione dei dati nella tabella appena creata

Dim objConn As New SqlConnection(My.Settings.ConnectionString1.ToString)
objConn.Open()
Dim da_temp As SqlDataAdapter
da_temp = New SqlDataAdapter(“Select top 1 * From [" & ds.DataSetName & "]“, objConn)

da_temp.AcceptChangesDuringFill = True
Dim ds_temp As New DataSet()
da_temp.FillSchema(ds_temp, SchemaType.Source)
da_temp.Fill(ds_temp)

‘ Setto come nuove tutte le righe del DataSet

Dim riga_tmp As DataRow
For Each riga_tmp In ds.Tables(0).Rows
Try
riga_tmp.SetAdded()
Catch ex As Exception

End Try

Next

Try
ds_temp.Merge(ds, False)
Catch ex As Exception

End Try

Dim objCommandBuilder As New SqlCommandBuilder(da_temp)
da_temp.Update(ds_temp)

da_temp.Dispose()
objConn.Close()

Return 1
End Function
La funzione SysType2DBType richiamata nella linea 14 e’ la stessa realizzata nel post VB.NET: Convertire da System.Type a SqlDbType





VB.NET: importare un file CSV in un dataset

31 03 2008

Tra le possibilita’ offerta dalla classe OleDb del Framework .NET c’e’ quella di trattare un file CSV alla stregua di una tabella di un database, ed eseguire su di esso vere e proprie query in linguaggio SQL, importando poi i risultati all’interno di un dataset, utilizzabile poi per qualsiasi altro scopo.

Vediamo come realizzare quindi una breve funzione che, dato il path di un file CSV, ne restitusca il DataSet corrispondente.

Public Function CSV2DataSet(ByVal filename As String) As DataSet

Dim fFile1 As New FileInfo(filename)

‘creazione schema.ini (Necessario per il corretto riconoscimento del separatore, in questo caso il carattere tab)

Dim oFile As System.IO.File
Dim oWrite As System.IO.StreamWriter
oWrite = oFile.CreateText(fFile1.DirectoryName & “\schema.ini”)
oWrite.WriteLine(“[" & fFile1.Name & "]“)
oWrite.WriteLine(“Format=TabDelimited”)
oWrite.Close()

Dim strConn As String = “Provider=Microsoft.Jet.OLEDB.4.0;Extended Properties=’text;HDR=Yes;FMT=TabDelimited;’;Data Source=” & fFile1.DirectoryName

Dim ds As New DataSet(fFile1.Name)
Dim ds As New DataSet(tablename)
Dim conn As New OleDb.OleDbConnection(strConn)

Dim da As OleDb.OleDbDataAdapter

Dim mycmd As New OleDb.OleDbCommand(“Select * from ” & fFile1.Name, conn)

Try
da = New OleDb.OleDbDataAdapter(mycmd)
da.Fill(ds)

Catch ex As Exception

End Try

Return ds

End Function





VB.NET: Convertire da System.Type a SqlDbType

28 03 2008

Realizzando un progettino che richiedeva un uso ‘creativo’ dei DataSet (ne scriverò in futuro), mi son trovato nella situazione di dover convertire i tipi di dati contenuti appunto in un Dataset (che appartengono alla classe System.Type) nei relativi tipi contenuti nell’enum SqlDbType (contenuto nella classe System.Data).

Ho realizzato quindi una breve funzione che svolgesse tale compito:

Private Function SysType2DBType(ByVal SysType As System.Type) As SqlDbType
   Dim param1 As SqlClient.SqlParameter
   Dim typeconv1 As System.ComponentModel.TypeConverter
   param1 = New SqlClient.SqlParameter()
   typeconv1= System.ComponentModel.TypeDescriptor.GetConverter(param1.DbType)
   If tc.CanConvertFrom(SysType) Then
       param1.DbType = typeconv1.ConvertFrom(SysType.Name)
   End If
   Return param1.SqlDbType
End Function





ASP.NET: upload di files con progress bar

20 02 2008

Domanda di un lettore nei commenti del post sull’upload di files in asp.net:

simone (14:44:58)
ciao!,
sono alle prese anche io con l’upload di file su un server, pero in questo caso devo uploadare file di grossa dimensione, quindi vorrei creare una sorta di status bar per il file upload (che mi indicasse la percentuale di upload in tempo reale)…mi sai dare qualche consiglio?
grazie mlle per l’attenzione.

Oltre a rispondere al gentile Simone nei commenti, credo che sia utile mettere in evidenza la risposta in un post apposito.

Esiste un componente OpenSource per realizzare un sistema di upload molto completo, con progress bar visualizzabile sia all’interno della pagina che in un popup apposito: NeatUpload.

Ho avuto modo di utilizzarlo gia’ dalle primissime versioni: quella attuale e’ la 1.2.26 e noto con piacere che con l’autentare del numero di versione e’ aumentata anche la compatibilita’ con i vari browser esistenti e che l’interfaccia e’ stata aggiornata in maniera ‘ajax-oriented’ :-) (Demo)

L’utilizzo e’ semplicissimo: una volta inserito il componente sulla pagina, lo si puo’ instanziare e utilizzare come un normale controllo FileUpload. Il manuale e’ comunque molto esaustivo.





VB.NET: Scrivere nei log di sistema

19 02 2008

Scrivere nei log di sistema di windows puo’ essere molto utile, sopratutto quando si sviluppa un servizio o una applicazione che lavora in background senza riscontri ‘visuali’.

Nel mio precedente post sul Namespace My (prerogativa di VB.NET) segnalavo la funzione My.Application.Log.WriteEntry(), che permette di scrivere nei log dell’applicazione.
Tali log non sono per forza quelli di sistema: di norma sono quelli personali dell’applicazione, un semplice file di testo da andare a leggere successivamente.

Se volessimo fare in modo che il nostro software vada a scrivere direttamente negli eventi di sistema, quelli visionabili con l’utility EventViewer, dobbiamo apportare alcune modifiche al file di configurazione dell’applicazione, App.Config.

Andremo quindi a modificare prima la sezione Sources in System.Diagnostics, in questo modo:


<sources>
            <!-- This section defines the logging configuration for My.Application.Log -->
            <source name="DefaultSource" switchName="DefaultSwitch">
                <listeners>
                    <!--<add name="FileLog"/>-->
                    <!-- Uncomment the below section to write to the Application Event Log -->
                    <add name="EventLog"/>
                </listeners>
            </source>
</sources>

in seguito, apportiamo una modifica anche alla sezione SharedListeners:


<sharedlisteners>
 <!--<add name="FileLog" type="Microsoft.VisualBasic.Logging.FileLogTraceListener, Microsoft.VisualBasic, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" initializeData="FileLogWriter"/>-->
<!-- Uncomment the below section and replace APPLICATION_NAME with the name of your application to write to the Application Event Log -->
<add name="EventLog" type="System.Diagnostics.EventLogTraceListener" initializeData="NomeApplicazione"/>
</sharedlisteners>

sostituendo a ‘NomeApplicazione’ il nome scelto per l’applicazione, quello che la identifichera’ nell’Event Viewer.In questo modo, a ogni chiamata della funzione My.Application.Log.WriteEntry() verra’ creata una nuova entry nell’ApplicationLog di sistema.





ASP.NET: accedere a un controllo presente su una MasterPage da una ContentPage

6 02 2008

aspnet.pngChi sviluppa in ASP.NET si sara’ sicuramente accorto di quanto sia importante tenere bene a mente il ciclo di vita di una pagina: durante la creazione delle pagina stessa si susseguono una serie di eventi in un ordine ben definito, e tutto va realizzato proprio nel rispetto di tale ordine.

Nel caso la nostra applicazioni utilizzi le MasterPages la faccenda si complica un pochino, e benche’ sia possibile includere codice nella MasterPage spesso ci si trova nella situazione di dover agire dal codice presente su una ContentPage su un controllo presente invece nella pagina Master.

La procedura che utilizzo di solito ‘ riassunta in questo esempio:

Dim MP_TextBox As TextBox = CType(Page.Master.FindControl("TextBoxMaster"), TextBox)
MP_TextBox.Text = "TESTO"

Il funzionamento e’ presto spiegato: tramite la funzione FindControl mi ricavo una istanza del controllo di tipo TextBox presente sulla masterPage.
Tale istanza mi viene restituita come oggetto ‘grezzo’, quindi ne effettuo il cast in un oggetto TextBox utilizzando CType. A questo punto posso accedere tranquillamente al controllo.Tale procedura puo’ essere utilizzata anche per metter mano sulla MasterPage a controlli non strettamente legati al framework.
Un esempio puo’ essere quello di modificare il background del body (dichiarato quindi nella MP).
Per prima cosa e’ necessario dichiarare il body della MP come runat=”server”, e assegnargli un ID:


<body topmargin=0 leftmargin=0 runat=server id="bodyMaster" >

in seguito, dalla content page il codice sara’ il seguente:


Dim MP_Body As System.Web.UI.HtmlControls.HtmlGenericControl = CType(Page.Master.FindControl("bodyMaster"), System.Web.UI.HtmlControls.HtmlGenericControl)
MP_Body.Attributes("bgcolor") = "red"

Ovviamente, dell’oggetto ottenuto tramite FindControl andra’ effettuato il casting in HtmlGenericControl.





ASP.NET: realizzare lo screenshot di una pagina web

11 01 2008

Scenario: web application in Asp.NET per la gestione e pubblicazione di un piccolo magazine informativo ad uso interno.

Oltre al numero attuale, gli utenti possono accedere, da una apposita pagina di archivio, alle precedenti pubblicazioni.
Perche’ non realizzare qualcosa per creare in tempo reale uno screenshot del numero in archivio, ridurlo a piccolo thumbnail e affiancarlo alla descrizione del numero in elenco?

Di gia’ fatto non ho trovato niente, a parte un controllo ActiveX ‘commerciale’ e sinceramente molto poco economico, quindi ho deciso di realizzare qualcosa partendo da zero.

L’idea e’ questa: perche’ non utilizzare un controllo di tipo WebBrowser per caricare la pagina della quale vogliamo realizzare lo screenshot e poi ‘ritagliare’ il contenuto, salvando il tutto in una immagine?
Il tutto si puo’ fare anche in maniera non ‘visuale’, impacchettando tutto in una bella classe che automatizza la procedura: in questo modo l’oggetto puo’ essere usato indifferentemente in una web application o in una applicazione stand-alone.

Ho realizzato il tutto in C#: una volta impacchettato l’oggetto in una classlibrary ci si ritrova tra le mani un bel file .DLL da includere nella propria applicazione.

Di seguito il sorgente

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.IO;
using System.Reflection;

using WinHttp;

    public class WebSiteThumbnail
    {
        private string url = null;
        private Bitmap bmp = null;
        public Bitmap Image
        {
            get
            {
                return bmp;
            }
        }
        private ManualResetEvent mre = new ManualResetEvent(false);
        private int timeout = 5; // Da raffinare
        private int thumbWidth;
        private int thumbHeight;
        private int width;
        private int height;

        #region Metodi Statici
        public static Bitmap GetSiteThumbnail(string url, int width, int height, int thumbWidth, int thumbHeight)
        {
            WebSiteThumbnail thumb = new WebSiteThumbnail(url, width, height, thumbWidth, thumbHeight);
            Bitmap b = thumb.GetScreenShot();
            if (b == null)
                b = (Bitmap)System.Drawing.Image.FromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream("PAB.WebControls.Notavailable.jpg"));
            return b;
        }      

        #endregion

        #region Constructors
        public WebSiteThumbnail(string url, int width, int height, int thumbWidth, int thumbHeight)
        {
            this.url = url;
            this.width = width;
            this.height = height;
            this.thumbHeight = thumbHeight;
            this.thumbWidth = thumbWidth;
        }
        #endregion

        #region ScreenShot
        public Bitmap GetScreenShot()
        {
            Thread t = new Thread(new ThreadStart(_GetScreenShot));
            t.SetApartmentState(ApartmentState.STA);
            t.Start();
            mre.WaitOne();
            t.Abort();
            return bmp;
        }
        #endregion
        private void _GetScreenShot()
        {
            WebBrowser webBrowser = new WebBrowser();
            webBrowser.ScrollBarsEnabled = false;
            DateTime time = DateTime.Now;

            webBrowser.Navigate(url);

            webBrowser.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(WebBrowser_DocumentCompleted);
            while (true)
            {
                Thread.Sleep(0);
                TimeSpan elapsedTime = DateTime.Now - time;
                if (elapsedTime.Seconds >= timeout)
                {
                    mre.Set();
                }
                Application.DoEvents();
            }

        }
        private void WebBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
        {
            WebBrowser webBrowser = (WebBrowser)sender;
            webBrowser.ClientSize = new Size(this.width, this.height);
            webBrowser.ScrollBarsEnabled = false;
            bmp = new Bitmap(webBrowser.Bounds.Width, webBrowser.Bounds.Height);
            webBrowser.BringToFront();
            webBrowser.DrawToBitmap(bmp, webBrowser.Bounds);
            Image img = bmp.GetThumbnailImage(thumbWidth, thumbHeight, null, IntPtr.Zero);
            bmp = (Bitmap)img;
            webBrowser.Dispose();
            if (mre != null)
                mre.Set();
        }

        public void Dispose()
        {
            if (bmp != null) this.bmp.Dispose();
        }
    }

Veniamo a un esempio di utilizzo: una pagina .aspx che accetta come parametro l’url della pagina e restituisce uno stream con l’immagine della pagina.Realizzate quindi una nuova classlibrary, copiate il codice in GetSiteThumbnail.cs e compilate il tutto.Copiate poi la DLL generata nella directory ‘bin’ di un nuovo progetto web e realizzate un pagina chiamata (ad esempio) getsitethumb.aspx:

< %@ Import Namespace="System.Drawing" %>
< %@ Import Namespace="System" %>
< %@ Import Namespace="System.Web" %>
< %@ Import Namespace="WebSiteThumbnail" %>

< %@ Page MaintainScrollPositionOnPostback="false" %>
<html>
<script language="VB" runat="server">

  Sub Page_Load(Sender As Object, E As EventArgs)
        Dim immagine As System.Drawing.Bitmap
        If Request.QueryString("url" ) = "" Then Response.End()
        Dim thumb As New WebSiteThumbnail(Request.QueryString("url"), 715, 715, 100, 100)
        immagine = thumb.GetScreenShot()
        Response.ContentType = "image/jpeg"
        immagine.Save(Response.OutputStream, Imaging.ImageFormat.Jpeg)
        Response.End()
    End Sub
</script>
</html>

A questo punto, richiamando http://sito/getsitethumb.aspx?url=http://www.google.it visualizzerete sul browser un thumbnail di 1000×100px contenente una sezione di 715×715px dell’homepage di google.L’utilizzo all’interno di una normale pagina web e’ quindi il seguente:

<img src=”http://sito/getsitethumb.aspx?url=http://www.google.it”>





Visual Studio 2008, novità interessanti e alcuni dubbi

31 12 2007

bgvs.jpgDopo un paio di versioni beta, Microsoft ha finalmente rilasciato la versione definitiva di Visual Studio 2008, accompagnato dalla nuova versione del Framework .NET, la 3.5

Vediamo nel dettaglio le novità e partiamo subito con una mia perplessità a riguardo: una discreta parte delle nuove funzionalità dedicate allo sviluppo di applicazioni desktop sono orientate all’utilizzo con windows Vista e Office 2007.
Sostanzialmente, tutti i vantaggi derivati dall’utilizzo del nuovo ambiente di sviluppo per la realizzazione di applicazioni stand-alone potranno essere sfruttate solamente se il target di sviluppo è una piattaforma con le ultimissime versioni di Windows e Office.

VS2008 introduce infatti il supporto per Windows Presentation Foundation il sottosistema grafico di Windows Vista: utilizzando il WPF, la realizzazione dell’interfaccia utente in una applicazione avviene tramite la scrittura di codice XAML (eXtensible Application Markup Language), un linguaggio di markup basato su XML (nell’universo opensource il suo equivalente è XUL, derivato dal progetto Mozilla).

L’utilizzo del designer WPF integrato in VS2008 è molto simile a quello del vecchio ambiente di disegno dei Windows Forms, con in aggiunta la possibilità di lavorare in ‘SplitView’, con la finestra di design affiancata dal codice XAML relativo, come se si stesse sviluppando l’interfaccia di una web application: lo scopo finale a mio avviso è proprio quello di ridurre sempre di più le differenze tra lo sviluppo di applicazioni desktop e web, non per niente la stessa Microsoft sta spingendo per rendere XAML l’erede dell’ormai ‘vecchio’ HTML.

Incluso nella nuova versione di VS anche il supporto per lo sviluppo di applicazioni con Microsoft Office: compatibile con Office 2003 da tuttavia del suo meglio con il nuovo Office 2007, permettendo di sviluppare applicazioni native che sfruttano la user interface che caratterizza tale versione, come le ‘amate/odiate’ Ribbon Status Bar, Ribbon Bar e le Mini-Toolbar.

Il nuovo Framework 3.5 introduce anche la tecnologia Windows Communication Foundation, che in poche parole va a semplificare lo sviluppo delle applicazioni di rete unificando in una sola architettura tecnologie diverse come WebServices, Socket, Enterprice Service.
Una volta sviluppato un servizio WFC, da VS2008 è possibile effettuarne il debug senza realizzare una apposita applicazione Host, utilizzando due utility dedicate, il WFC Service Auto Host e il WFC Test Client.

Presente come estensione installabile in Visual Studio 2005, nella versione 2008 il supporto per Windows Workflow Foundation è presente nativamente.
WWF permette la progettazione e lo sviluppo di applicazioni legate a stati ben definiti: un esempio calzante lo si può trovare all’interno di un programma di contabilità. Lo stato di una fattura o di un ordine attraversa degli ’step’ ben definiti, utilizzando il WWF e il suo designer presente in VS2008 è possibile definire lo stato dei controlli presenti nell’interfaccia grafica legati a ogni singolo step.

L’accesso alle basi dati è stato rivoluzionato in questa nuova versione di Visual Studio dall’introduzione nel framework 3.5 del linguaggio LINQ (Language INtegrated Query).
Lo scopo di LINQ è di unificare il linguaggio di accesso ai dati: lo sviluppatore lo potrà utilizzare per scrivere query nativamente in VB o C# senza dover ricordare gli eventuali dialetti di SQL legati alla piattaforma di database utilizzata, trattando i dati presenti nel Db relazione come oggetti. Lontanamente è quello che in java si è iniziato a fare con Hibernate, seguito poi da molti ‘cloni’.

La realizzazione delle applicazioni web in VS2008 prende invece una piega interessante: .NET 3.5 porta con se infatti il supporto nativo per la programmazione AJAX, riallineando il framework Microsoft a molti altri presenti nel panorama OpenSource già dotati del supporto per ajax.
Con le nuove API è possibile sia sviluppare da zero una nuova applicazione AJAX, oppure migrare una precedentemente sviluppata alla nuova tecnologia utilizzando i controlli ScriptManager e UpdatePanel (vecchie conoscenze di chi ha iniziato in passato a ’spippolare’ con ATLAS e AJAX.NET).
Un’altra novità nell’ambiente di sviluppo è la presenza di completamento del codice, dell’intellisense e del debug per il codice Javascript ‘Client-Side’.

In definitiva il nuovo ambiente di sviluppo targato Microsoft a mio avviso è degno di uno sguardo piu approfondito, nonostante la fruizione di interessanti funzionalità sia limitata a clients Windows Vista+Office 2007.





Asp.net: personalizzazione delle webparts a livello di pagina

17 10 2007

Grande cosa le webparts in Asp.net 2.0: la possibilita’ di offrire interfacce personalizzabili agli utenti, con la possibilita’ per ognuno di salvare le proprie impostazioni e’ una gran bella cosa.

Tuttavia ci si puo’ trovare in una situazione simile alla mia:

piccolo portale realizzato con le webpart, con lo storage delle personalizzazioni su un Ms SQLServer 2000; l’idea e’ quella di permettere la personalizzazione delle pagine tramite le webparts, ma tali dati devono essere legati non all’utente che le realizza, ma alla singola pagina.

Di default la classe SQLPersonalizationProvider prevede personalizzazioni legate esclusivamente all’utente che le realizza, come fare a fare in modo che vengano salvate usando come discriminante l’indirizzo della pagina?
Semplice, ‘illudendo’ il personalization provider che il nome della pagina sia lo username!

Tralasciando la realizzazione di una pagina funzionante con le webparts (si trovano in rete decine di tutorials,e cmq si presume che chi legga questo articolo quel passo lo abbia gia’ superato) passiamo alla realizzazione di una classe custom derivata da SQLPersonalizationProvider:


Imports Microsoft.VisualBasic

Public Class CustomSqlPersonalizationProvider
Inherits SqlPersonalizationProvider

Protected Overrides Sub LoadPersonalizationBlobs(ByVal _webPartManager As WebPartManager, ByVal _path As String, ByVal _userName As String, ByRef _sharedDataBlob() As Byte, ByRef _userDataBlob() As Byte)
Dim pagina As String
pagina = HttpContext.Current.Request.QueryString("page")
MyBase.LoadPersonalizationBlobs(_webPartManager, _path, pagina, _sharedDataBlob, _userDataBlob)
End Sub

Protected Overrides Sub ResetPersonalizationBlob(ByVal _webPartManager As WebPartManager, ByVal _path As String, ByVal _userName As String)
Dim pagina As String
pagina = HttpContext.Current.Request.QueryString("page")
MyBase.ResetPersonalizationBlob(_webPartManager, _path, pagina)
End Sub

Protected Overrides Sub SavePersonalizationBlob(ByVal _webPartManager As WebPartManager, ByVal _path As String, ByVal _userName As String, ByVal _dataBlob() As Byte)
Dim pagina As String
pagina = HttpContext.Current.Request.QueryString("page")
MyBase.SavePersonalizationBlob(_webPartManager, _path, pagina, _dataBlob)
End Sub

End Class

Di cosa si tratta? La nuova classe ereditata da SQLPersonalizationProvider esegue l’override dei metodi dedicati al salvataggio, caricamento e reset delle impostazioni e li altera in modo da utilizzare come identificativo per i dati salvati sul DB il valore del parametro ‘page’ passato in querystring anziche lo username dell’utente collegato.

A questo punto, dobbiamo modificare il web.config in modo da far utilizzare questa nuova classe al posto della tradizionale:

<webparts>
<personalization defaultProvider="CustomSqlPersonalizationProvider">
<providers>
<add name="CustomSqlPersonalizationProvider"
type="CustomSqlPersonalizationProvider"
connectionStringName="SQLConnString"
applicationName="/" />

</providers>

<authorization>
<deny users="*" verbs="enterSharedScope" />
<allow users="*" verbs="modifyState" />
</authorization>
</personalization>
</webparts>

Passando quindi alla nostra pagina il parametro ‘page’ dalla querystring potremo differenziare le personalizzazioni:

le modifiche effettuate su http://sito/default.aspx?page=pagina1 saranno quindi separate e distinte da quelle su http://sito/default.aspx?page=pagina2 e cosi’ via.

interessante notare che con questo metodo la creazione della nuova pagina avviene come su un wiki: cercando infatti di accedere a una pagina attualmente non presente viene presentata una pagina con le webpartzones vuote, pronte per essere ‘riempite’.





ASP.NET: upload di foto con creazione di thumbnails

29 09 2007

Oggi vi propongo un piccolo frammento di codice in vb.net che permette di effettuare da una pagina aspx l’upload di una foto e la creazione del relativo thumbnail.

Per prima cosa, inseriamo sulla pagina un controllo FileUpload e un pulsante che darà il via al processo di upload:

<asp:FileUpload ID=”FileUpload1″ runat=”server” />
<asp:Button ID=”Button1″ runat=”server” Text=”Carica Immagine” />

in seguito nel CodeBehind relativo al click sul pulsante realizziamo l’upload e la creazione del ThumbNail:


Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
If Not FileUpload1.PostedFile Is Nothing And FileUpload1.PostedFile.ContentLength > 0 Then
Dim unixtime As String
unixtime = (DateTime.UtcNow - New DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds
unixtime = unixtime.Replace(",", "")
'Dim fn As String = System.IO.Path.GetFileName(FileUpload1.PostedFile.FileName)
Dim fn As String = unixtime &amp; "-" &amp; System.IO.Path.GetFileName(FileUpload1.PostedFile.FileName)
Dim SaveLocation As String = Server.MapPath("foto") &amp;  "\\\\" &amp;  fn

Try
FileUpload1.PostedFile.SaveAs(SaveLocation)
'Creazione Miniatura
Dim img As System.Drawing.Bitmap
Dim imgNew As System.Drawing.Image
Dim inp As New IntPtr()

img = img.FromFile(SaveLocation)
imgNew = img.GetThumbnailImage(100, 62, Nothing, inp)
imgNew.Save(Server.MapPath("foto") &amp; "\\\\thumbs\\\\" &amp; fn, System.Drawing.Imaging.ImageFormat.Jpeg)

'Codice per eventuale insert in database

Catch Exc As Exception
Response.Write("Error: " &amp; Exc.Message)
End Try
Else
Response.Write("Please select a file to upload.")
End If
End Sub

Finito! La foto originale verrà caricata nella drectory ‘foto’, mentre la miniatura realizzata utilizzando il metodo GetThumbnailImage (grazie a Gianpiero per l’esaustivo tutorial sull’utilizzo) verrà creata in ‘foto/thumbs’.