Home

 
 
 
 
 



 
 
 
 

 
 
 

 
 
 
 
 









Blog2theMax
Il blog del team di Code Architects

In Visual Basic 6 esiste una comodissima proprietà dell'oggetto App che permette di determinare se vi sono altre istanze in esecuzione della stessa applicazione. Manco a dirlo, questa proprietà non è mai stata migrata in VB.NET anche se, per fortuna, una applicazione scritta in VB 2005 può almeno usare l'evento StartupNextInstance:

' to display this code, open the Application page of the My Project designer and click the Application Events button
Namespace
My
  
Partial Friend Class MyApplication
      Private Sub MyApplication_StartupNextInstance(ByVal sender As Object, ByVal e As Microsoft.VisualBasic.ApplicationServices.StartupNextInstanceEventArgs) Handles Me.StartupNextInstance
         ' another instance of this application has been launched
      End Sub
  
End Class
End
Namespace

Il problema di questo approccio è che l'evento viene sollevato nella prima istanza della applicazione, non nella nuova istanza, quindi alla partenza una applicazione VB.NET non può sapere con certezza se è l'unica istanza o meno. In altre parole, può solo sapere se e quando un'altra applicazione parte, non se l'applicazione corrente è la prima e unica istanza in esecuzione.

In .NET 2.0 è stato introdotto il nuovo tipo System.Threading.Semaphore che permette di risolvere il problema in modo davvero elegante. Un semaforo è un oggetto che può essere incrementato e decrementato. La cosa interessante è che se il semaforo ha un nome, allora esso è condiviso da tutte le applicazioni .NET in esecuzione che richiedono un riferimento al semaforo con quel particolare nome. Basta quindi che l'applicazione crei alla partenza un semaforo con un nome univoco (ad es. un nome che include il percorso dell'eseguibile, che è quasi certamente univoco) e più applicazioni possono condividere il contatore interno al semaforo. L'unico problema che resta da risolvere è assicurarsi che il valore del semaforo sia correttamente ripristinato quanto l'applicazione termina, ma anche questo è facile da ottenere usando un metodo Finalize.

In aggiunta alla proprietà PrevInstance - che restituisce False se l'applicazione era l'unica istanza al momento della partenza - la seguente classe VB6App espone anche la proprietà InstanceCount, che restituisce il numero totale di istanze in esecuzione in quel momento, incluso quindi l'applicazione corrente. Ecco il codice della classe VB2005:

Class VB6App
   ' the default instance
  
Private Shared DefValue As New VB6App

   ' the system-wide semaphore
  
Private semaphore As System.Threading.Semaphore
   ' initial count for the semaphore (very high value)
  
Private Const MAXCOUNT As Integer = 10000

   Private Sub New()
      Dim ownership As Boolean = False
      ' create a unique name, but strip invalid characters
     
Dim name As String = "VB6App_" & System.Reflection.Assembly.GetExecutingAssembly().Location.Replace(":", "").Replace("\", "")
      semaphore = New System.Threading.Semaphore(MAXCOUNT, MAXCOUNT, name, ownership)
      ' decrement its value 
      semaphore.WaitOne()
      ' if we got ownership, this app has no previous instances
      m_PrevInstance = Not ownership
   End Sub

   ' the PrevInstance property returns True if there was a previous instance running 
  
' when the default instance was created
  
Private Shared m_PrevInstance As Boolean

   Public Shared ReadOnly Property PrevInstance() As Boolean
      Get
         Return m_PrevInstance 
      End Get
   End Property

   ' return the total number of instances of the same application that are currently running 
  
Public Shared ReadOnly Property InstanceCount() As Integer
      Get
         ' release the semaphore and grab the previous count 
        
Dim prevCount As Integer = DefValue.semaphore.Release()
         ' acquire the semaphore again
        
DefValue.semaphore.WaitOne()
         ' eval the number of other instances that are currently running 
        
Return MAXCOUNT - prevCount
      End Get
   End Property

   Protected Overrides Sub Finalize()
      ' increment the semaphore when the application terminates
     
semaphore.Release()
   End Sub

End Class

Notate che questa classe contiene un metodo Finalize senza implementare IDisposable. Si tratta di uno dei casi speciali in cui è giusto violare il pattern Dispose-Finalize.

Per comprende come funziona la classe, basta ricordare che il metodo Release dell'oggetto Semaphore incrementa il valore interno, mentre WaitOne lo decrementa. L'unica accortezza da usare con questo codice è di testare la proprietà VB6App.PrevInstance il prima possibile, ad esempio nel metodo Sub Main o nell'evento Load del form principale, per dare la possibilità alla classe di conservare il suo valore alla partenza del programma. Lo stesso form potrebbe poi testare il valore di InstanceCount in uscita, ad esempio se è necessario eseguire un codice di cleanup quando l'ultima istanza della applicazione termina:

Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
   If Not VB6App.PrevInstance Then 
      ' open the common log file
      ' ...
   End
If
End Sub

Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
   If VB6App.InstanceCount = 1 Then
     
' close the common log file.
      ' ...
  
End If
End Sub

Visto che l'utilità di questa classe, l'ho anche tradotta in C#:

public class VB6App
{
   // the default instance 
   private static VB6App DefValue = new VB6App(); 
   // the system-wide semaphore
   private System.Threading.Semaphore semaphore; 
   // initial count for the semaphore (very high value)
   private const int MAXCOUNT = 10000;

   private VB6App() 
   { 
      // create a named (system-wide semaphore)
      bool ownership = false
      // create the semaphore or get a reference to an existing semaphore

      string name = "VB6App_" + System.Reflection.Assembly.GetExecutingAssembly().Location.Replace(":", "").Replace("\", "");
      semaphore = new System.Threading.Semaphore( MAXCOUNT, MAXCOUNT, name, ref ownership); 
      // decrement its value
     
semaphore.WaitOne(); 
      // if we got ownership, this app has no previous instances
      m_PrevInstance = !ownership;

   }

   // the PrevInstance property returns True if there was a previous instance running
  
// when the default instance was created

   private static bool m_PrevInstance ;

   public static bool PrevInstance 
  
      get 
     
         return m_PrevInstance ; 
     
   }

   // return the total number of instances of the same application that are currently running

   public static int InstanceCount 
  
      get 
     
         // release the semaphore and grab the previous count 
        
int prevCount = DefValue.semaphore.Release(); 
         // acquire the semaphore again
        
DefValue.semaphore.WaitOne(); 
         // eval the number of other instances that are currently running 
        
return MAXCOUNT - prevCount; 
     
   }

   ~VB6App() 
  
      // increment the semaphore when the application terminates
     
semaphore.Release(); 
   }
}

 

5/27/2006 11:32:35 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

In pochi giorni mi sono arrivate due mail da altrettanti lettori, che chiedono come sia possibile - in una finestra Windows Forms che sfrutta l'evento Validating dei controlli per validare l'input - saltare la validazione se l'utente chiude il form. Se non si salta la validazione, infatti, l'effetto è che l'utente potrebbe non riuscire a chiudere il form. La risposta è molto semplice, ma evidentemente non è proprio ovvia, quindi la ripeto qui a beneficio di tutti.

Se l'utente chiude un form cliccando sul pulsante "X" o scegliendo "Close" dal menu di sistema, scatta prima l'evento Validating del controllo che ha il focus poi, anche se il primo evento ha decretato che la validazione è fallita, scatta l'evento FormClosing. La cosa interessante è che, se l'evento Validating ha impostato e.Cancel = True, nell'evento FormClosing è possibile resettare questo valore a False. In definitiva, quindi, per evitare che il codice nell'evento Validating impedisca all'utente di chiedere il form sono sufficienti queste istruzioni:

Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As FormClosingEventArgs) Handles Me.FormClosing
   If e.CloseReason = CloseReason.UserClosing OrElse e.CloseReason = CloseReason.WindowsShutDown Then
      e.Cancel = False
   End If
End Sub

Un modo più radicale per evitare il problema è di nascondere il pulsante "X" oppure il comando "Close" del menu di sistema della finestra, impostando la proprietà ControlBox a False. Oppure potete disattivarla (magari anche solo temporaneamente) con il seguente codice:

Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
   SetCloseCommandState(Me.Handle, False)
End Sub

Private Declare Function GetSystemMenu Lib "user32" (ByVal hWnd As IntPtr, ByVal bRevert As Integer) As IntPtr
Private Declare Function EnableMenuItem Lib "user32" (ByVal hMenu As IntPtr, _
   ByVal wIDEnableItem As Integer, ByVal wEnable As Integer) As Integer
Const MF_GRAYED As Integer = &H1&
Const MF_BYCOMMAND As Integer = &H0&
Const SC_CLOSE As Integer = &HF060&

Sub SetCloseCommandState(ByVal hWnd As IntPtr, ByVal newState As Boolean)
   Dim hMenu As IntPtr = GetSystemMenu(hWnd, 0)
   Dim wFlags As Integer = MF_BYCOMMAND
   If newState Then
     
wFlags = wFlags And Not MF_GRAYED
   Else
     
wFlags = wFlags Or MF_GRAYED
   End If
  
EnableMenuItem(hMenu, SC_CLOSE, wFlags)
End Sub

5/13/2006 12:42:44 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

UPDATE: dopo aver messo online questo articolo ho modificato leggermente la libreria CAPermutations, e il link ora punta alla versione aggiornata. Tutto quanto scritto nel post originario resta valido, eccetto che il terzo argomento da passare al costruttore è ora un enumerativo anzichè un booleano.

Da quando ho potuto mettere le mani su un computer sono sempre stato affascinato dai problemi combinatoriali. In prima approssimazione, un problema combinatoriale prevede la scelta di una (o più) permutazioni o combinazioni di N elementi in modo da soddisfare alcuni vincoli. E' facile intuire che molti problemi si risolvono solo grazie a tecniche combinatoriali:

  • La generazione di tutti gli anagrammi di una parola richiede la generazione di tutte le permutazioni possibili dei suoi caratteri, per poi scegliere tra le parole ottenute quelle di senso compiuto
  • Un programma per generare sistemi di Totocalcio, Lotto, Totip e simili richiedono la generazione di tutte le colonne possibili, da cui vengono poi scelte quelle che soddisfano alcuni requisiti (tipo numero minimo e massimo di segni, ecc.)
  • Un programma per determinare l'orario scolastico, oppure più in generale, per stabilire turni di lavoro richiede di generare tutte le possibili assegnazioni (persona,turno) per poi scegliere quelle che soddisfano i vincoli impostati dall'utente
  • Molti problemi matematici si basano sulla generazione di tutte le possibili permutazioni o combinazioni di N elementi, ad esempio la generazione degli quadrati magici di ordine N oppure la risoluzione del famoso problema delle regine (ovvero: come sistemare N regine su una scacchiera N*N in modo che nessuna dia scacco ad un'altra)
  • Lo stesso vale per molti giochi di tipo enigmistico, come ad esempio il Sodoku oppure le parole crociate.
  • Il problema dello zaino: abbiamo N oggetti di peso o ingombro differenti da trasportare in uno zaino che ha una capacità (come peso o volume) massima pari a un certo valore: quali oggetti scegliere in modo che la capacità dello zaino sia sfruttata al massimo, in modo cioè che la somma dei volumi o dei pesi degli oggetti scelti sia la più vicina possibile (ma non superiore) alla capacità dello zaino? Se pensate che invece di uno zaino avete a che fare con un container, vedete che il problema ha delle implicazioni pratiche non indifferenti
  • Il modo migliore per tagliare sagome bidimensionali da una superficie piana si determina provando i diversi modi di combinare le varie sagome in modo da minimizzare il materiale non utilizzato (si pensi ad esempio al problema di tagliare in modo ottimale le pelli di una sedia o un divano a partire dal pellame a disposizione ed evitando i punti in cui il pellame non è utilizzabile o presenta imperfezioni)
  • Il percorso ottimale per andare dalla località A alla località B in trenoo aereo può essere risolto combinando i vari percorsi intermedi, in modo da ridurre la distanza complessiva, il costo complessivo dei biglietti, il numero delle coincidenze aeree o ferroviarie, oppure ancora il tempo speso aspettando tali coincidenze. (Pensate ad esempio a come prenotate un viaggio con Expedia.)
  • La generazione di permutazioni e combinazioni è anche alla base di numerose tecniche di "attacco" per scardinare alcuni sistemi di sicurezza. Ad esempio, i programmi che tentano di trovare le password per un sito funzionano combinando le parole in un dizionario che contiene le parole e nomi propri più comuni.

Potrei continuare con altri esempi, ma credo che sia chiaro il fatto che le tecniche combinatorie possono essere utili in moltissimi casi. Per saperne di più, non dovete fare altro che gogglare un po' su termini come "permutations" o "combinations" e scoprirete un sacco di cose interessanti. (La ricerca per "permutations" restituisce circa 10 milioni di hit, quella per "combinations" oltre 137 milioni!).

In questi anni ho scritto numerosi programmi che richiedevano la generazione di permutazioni o combinazioni di elementi simili. Forse il primo programma di questo tipo è stato un generatore di sistemi per il totocalcio che girava su Sinclair Spectrum. Uno dei più recenti è stato il Sodoku Solver che risolve automaticamente gli schemi di Sodoku più complessi in qualche frazione di secondo, direttamente dal vostro broswer. Nei vent'anni intercorsi tra questi due programmi, ho usato tecniche combinatoriali per ottimizzare la soluzione di alcuni problemi come quelli citati sopra.

Una delle cose più intriganti di un problema combinatoriale è il fatto che anche problemi abbastanza semplici richiedono spesso soluzioni complesse dal punto di vista computazionale, e soprattutto richiedono tanto tempo di CPU a meno di non ottimizzare al meglio l'algoritmo. Le possibili permutazioni di 10 oggetti distinti è pari al fattoriale di 10, ovvero 1*2*3*4*5*6*7*8*9*10 = 3.628.800. Disporre i numeri 1-16 su una griglia 4x4 per vedere quali combinazioni generano un quadrato magico (ossia un quadrato in cui la somma lungo le righe, colonne, e diagonali è costante) richiede l'analisi di 20mila miliardi di posizioni, e cosi' via. Per riuscire a risolvere questi problemi in tempi accettabili si richiede spesso di ottimizzare il codice al massimo e di trovare delle tecniche di "potatura" in modo da poter scartare interi gruppi di soluzioni prima di doverle analizzare una a una. Anche in questo modo, la risoluzione di un quadrato magico di ordine 5 può richiedere alcuni giorni anche sulle CPU più performanti. Per avere una idea di quante varianti vi siano per questi problemi combinatoriali, date una occhiata al sito The Combinatorial Object Server.

Ogni problema combinatoriale è una storia a sè stante, nel senso che il codice per trovare le soluzioni di uno schema di Sodoku è molto diverso dal codice per generare l'orario scolastico ottimale o per trovare il tragitto aereo migliore. D'altra parte, queste applicazioni hanno anche molti tratti in comune. In particolare, la parte che genera le varie permutazioni o combinazioni di elementi è spesso simile. Non uguale, ma simile. Quello che cambia in ciascun caso sono le routine che controllano se una combinazione è valida o meno, oppure le funzioni che procedono alla "potatura" degli alberi delle soluzioni. Alla fine ho pensato: perchè non estrapolare l'algoritmo di generazione delle combinazioni in modo da poterlo riutilizzare in occasioni differenti? Il risultato è la libreria CAPermutations, ovvero l'oggetto di questo articolo e di alcuni altri post che seguiranno.

La libreria in questione contiene solo una classe, Permutations, che però già in questa prima versione è in grado di risolvere la maggior parte dei problemi citati in precedenza. Per i problemi non particolarmente complessi, utilizzare questa libreria è davvero semplice: si crea una istanza della classe e si passa al costruttore un vettore contenente tutti gli elementi da permutare, poi si entra in un ciclo For Each che permette di enumerare tutte le possibili permutazioni. La classe Permutations usa i generics, in modo da accettare e restituire un array tipizzato contenente gli elementi che devono essere (o che sono stati) combinati:

' generate all the permutation of the characters A,B,C,D
Dim elements() As Char = {"A"c, "B"c, "C"c, "D"c}
Dim perms As New Permutations(Of Char)(elements)
For Each chars() As Char In perms
   ' create and display the string obtained by concatenating the characters in the result
  
Console.Write(New String(chars) & ", ")
Next

Il codice C# è altrettanto semplice:

// generate all the permutation of the characters A,B,C,D
char elements[] = {"A"c, "B"c, "C"c, "D"c};
Permutations<Char> permutations = New Permutations<Char>(elements);
foreach ( char[] chars in perms )
{
   // create and display the string obtained by concatenating the characters in the result
  
Console.Write(new string(chars) + ", ");
}

Ecco il risultato che appare nella finestra di console, ovvero tutte le possibili permutazioni degli N elementi forniti in input:

ABCD, ABDC, ACBD, ACDB, ADBC, ADCB, BACD, BADC, BCAD, BCDA, BDAC, BDCA, CABD, CADB, CBAD, CBDA, CDAB, CDBA, DABC, DACB, DBAC, DBCA, DCAB, DCBA,

Invece di un loop For Each potete anche usare il metodo GetAllPermutations, che restituisce in un colpo solo tutte le pemutazioni. Poichè ogni permutazione è un vettore di tipo T (dove T è definito al momento di istanziare la classe generica Permutations), allora il risultato di questo metodo è un jagged array di tipo T, ovvero un vettore dove ciascun elemento è a sua volta un vettore di tipo T.

' VB
Dim
results()() As Char = perms.GetAllPermutations()

// C#
char[][] results = perms.GetAllPermutations();

La classe Permutations è anche in grado di determinare le permutazioni di un gruppo di K elementi presi dall'insieme di N elementi forniti in input, con K <= N . Per ottenere tali permutazioni basta passare al costruttore un secondo argomento, pari al numero K di elementi che devono apparire nel risultato:

' generate all the permutation of two characters chosen from the charaters A,B,C,D
Dim elements() As Char = {"A"c, "B"c, "C"c, "D"c}
Dim perms As New Permutations(Of Char)
(elements, 2)
' .... for each loop as before

Ecco il risultato generato dal ciclo:

AB, AC, AD, BA, BC, BD, CA, CB, CD, DA, DB, DC,

La classe Permutations permette di risolvere una numerosa classe di problemi combinatori e statistici. Ad esempio, se A,B,C,D rappresentano 4 città, l'insieme delle permutazioni rappresentano tutti i possibili percorsi che uniscono le città e che non ripassano mai da una città: un programma può facilmente analizzare questi percorsi per trovare quello che richiede meno tempo o ha un costo minore. Se invece gli elementi rappresentano possibili azioni - ad esempio, l'azione di sistemare una pedina su una determinata casella delle scacchiera - allora l'insieme delle permutazioni permette di stabilire quale sequenza di azioni tra quelle possibili permettono di ottenere il risultato migliore (ad esempio, vincere una partita di tris). E' importante notare che il risultato non conterrà due elementi uguali, il che è corretto perchè non vogliamo visitare due volte la stessa città e non possiamo sistemare due pedine nella stessa casella.

La classe Permutations permette anche di generare le combinazioni di K oggetti presi da un universo di N oggetti, come al solito con K <= N. L'unica differenza tra permutazioni e combinazioni è che con queste ultime l'ordine è ininfluente, quindi ad esempio la soluzione ABC è considerata equivalente a ACB, BAC, BCA, CAB, e CBA perchè quello che conta sono gli elementi che compaiono nel risultato e non il loro ordine. Le combinazioni permettono di risolvere tipi di problemi differenti da quelli visti finora. Per esempio, le combinazioni degli elementi {Giuseppe,Francesco,Marco,Piero,Gianni,MariaTeresa} con K=2 potrebbero servire per generare il calendario della prima serie di partite di un torneo di tennis a cui partecipano sei membri del team di Code Architects, in modo che ciascuna persona si batta una volta contro tutte le altre. In questo caso occorre usare le combinazioni perchè non occorre disputare la partita Francesco-Giuseppe se si è già giocata la partita Giuseppe-Francesco. (In altre parole, l'ordine degli elementi è ininfluente.)

Per generare combinazioni anzichè permutazioni, è sufficiente passare un valore enumerativo PermutationKind come terzo argomento al costruttore della classe Permutations. Ecco ad esempio come generare le partite del torneo:

Dim elements() As String = {"Giuseppe", "Francesco", "Marco", "Piero", "Gianni", "MariaTeresa"}
Dim perms As New Permutations(Of String)(elements, 2, PermutationKind.Combinatio )  ' anzichè PermutationKind.Permutations
For Each elem() As String In perms
   ' each element in the result is an array with two elements
  
Console.WriteLine("{0} - {1}", elem(0), elem(1))
Next

Ed ecco il risultato che appare nella console window.

Giuseppe - Francesco
Giuseppe - Marco
Giuseppe - Piero
Giuseppe - Gianni
Giuseppe - MariaTeresa
Francesco - Marco
Francesco - Piero
Francesco - Gianni
Francesco - MariaTeresa
Marco - Piero
Marco - Gianni
Marco - MariaTeresa
Piero - Gianni
Piero - MariaTeresa
Gianni - MariaTeresa

Fin quì niente di particolarmente eccitante, visto che è facile ottenere lo stesso risultato con qualche ciclo innestato di C# o VB, soprattutto se i valori di K e N sono costanti. Con K o N variabili occorre prevedere degli array che gestiscono gli indici dei vari loop, ma è un codice alla portata di tutti.

Le cose cominciano a diventare interessanti quando l'insieme degli elementi contiene elementi uguali, che non possono quindi essere considerati distinti quando si generano le permutazioni o le combinazioni. L'esempio classico è la generazione di anagrammi: il numero totale di anagrammi della parola "case" è 23, dato dal numero di pemutazioni possibili di 4 lettere (=1*2*3*4) meno uno per evitare di conteggiare la parola originale. D'altra parte, il numero di anagrammi della parola "casa" è soltanto 11, perchè la parola originale contiene due lettere uguali e quindi il numero di permutazioni distinte che è possibile creare è pari a 12, non 24. Questo particolare inizia a complicare non poco la struttura di un programma C# o VB che risolve il problema specifico, ma la classe Permutations rimuove automaticamente le ripetizioni dal risultato:

Ecco il codice nell'evento Click nel pulsante "Show anagrams" del programma demo:

' prepare to generate all permutations of all the characters in the word
Dim chars() As Char = txtWords.Text.ToCharArray()
Dim permutations As New Permutations(Of Char)(chars)
lstAnagrams.Items.Clear()
For Each chrs() As Char In permutations
   Dim anagram As String = New String(chrs)
   ' don't include the original word in the result
  
If anagram <> txtWords.Text Then lstAnagrams.Items.Add(anagram)
Next
lblMessage.Text = String.Format("Found {0} anagrams", lstAnagrams.Items.Count)

Come ho fatto notare prima, nella maggior parte dei casi quando chiediamo le permutazioni o combinazioni di un gruppo di elementi non vogliamo che lo stesso elemento compaia più volte. Ad esempio, se un elemento rappresenta la posizione di una regina posta sulla scacchiera, l'elemento non puo' comparire più di una volta perchè una casella non puo' contenere più pezzi. Altri problemi combinatori però non hanno questa limitazione. Se ad esempio stiamo calcolando tutte le possibili permutazioni di due dadi, dovremo includere anche i risultati in cui vi sia un doppio uno, un doppio due, ecc. Per ottenere questo risultato occorre specificare un valore maggiore di 1 come quarto argomento del costruttore della classe Permutations:

' get all the possible permutations of two dice
Dim elements() As Integer = {1, 2, 3, 4, 5, 6}
' we pass 2 as fourth argument because we accept that the same value can appear twice
Dim perms As New Permutations(Of Integer)(elements, 2, PermutationKind.Permutations , 2)
For Each dice() As Integer In perms
   Console.WriteLine("{0} {1}", dice(0), dice(1))
Next

In una prima versione della libreria avevo usato un booleano per indicare se le ripetizioni erano ammesse o meno, ma usare un intero ha il vantaggio di permettere una maggiore flessibilità. Ad esempio, è possibile generare tutte le permutazioni di tre dadi in cui lo stesso valore compare al massimo due volte:

Dim perms As New Permutations(Of Integer)(elements, 3, PermutationKind.Permutations, 2)

Queste prime note dpvrebbero essere sufficienti per cominciare a utilizzare la libreria CAPermutations, ma le sue possibilità sono di gran lunga superiori e fornirò qualche altro esempio nei prossimi giorni, man mano che ne creo. Nel frattempo, potete scaricare la libreria e il piccolo programma demo visibile nella figura da questo link: PermutationsDemo1.zip (23.15 KB)

Una ultima, importante nota: questa versione della libreria CAPermutations è disponibile come DLL compilata e può essere usata esclusivamente nei programmi freeware o no-profit. Se avete dubbi su questa forma di licenza o se avete la necessità di utilizzarla in applicazioni commerciali, contattatemi via email.

5/6/2006 12:01:04 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Ieri mi scrive un mio lettore, Claudio Fontana, con una richiesta apparentemente semplice: come fare per evitare sfarfallii strani quando si devono aggiornare la maggior parte dei controlli sul form? Il problema si fa sentire soprattutto quando occorre aggiungere migliaia di elementi a listbox e treeview.

In VB6 questo problema si risolveva molto semplicemente impostando la proprietà Visible (o Enabled) a False durante l'aggiornamento, e re-impostandola a True alla fine delle operazioni: la cosa interessante è che, a meno di non inserire una istruzione Refresh esplicita, il controllo non veniva effettivamente reso invisibile o "grayed", ma il suo nuovo contenuto appariva di botto, senza alcun effetto di flickering. Non solo: se il controllo veniva reso invisibile l'aggiornamento veniva anche eseguito molto più velocemente, tipicamente nella metà del tempo solitamente necessario. Sfortunatamente, in .NET il trucco di rendere i controlli temporaneamente invisibili non funziona più, nel senso che il refresh avviene effettivamente e quindi il controllo scompare effettivamente per tutta la durata dell'aggiornamento. Occorre pensare a qualcos'altro.

Alcuni controlli Windows Forms espongono i metodi BeginUpdate e EndUpdate, che di fatto ottengono non solo di "congelare" un controllo durante le operazioni di aggiornamento ma anche di sveltire notevolmente le operazioni stesse, che diventano anche 2.5 volte più veloci. Però solo quattro controlli espongono questa proprietà: ListBox, ComboBox, TreeView e ListView. Se il vostro form contiene molti controlli di altro tipo è necessario pensare a qualcos'altro, e questo è il problema che mi ha sottoposto Claudio, dopo aver googlato a destra e a manca sulla Rete alla ricerca di una soluzione, senza alcun esito.

Poichè il problema era intrigante, ho deciso di dedicarci qualche minuto, e sono arrivato ad una soluzione abbastanza interessante e (se Claudio ha googlato bene) anche inedita. L'idea è davvero molto semplice: (1) si "fotografa" l'aspetto corrente del form, copiandolo pixel per pixel in una bitmap, (2) si crea un controllo PictureBox grande quanto il form e si carica la bitmap nella PictureBox, (3) si aggiunge la PictureBox alla collection Controls e la si porta in primo piano, in modo da coprire tutti gli altri controlli, (4) mentre l'utente vede l'immagine congelata del form, si aggiornano tutti i controlli, usando i metodi BeginUpdate/EndUpdate se possibile per velocizzare le operazioni, (5) quando l'update è completato, si elimina la PictureBox in modo che l'utente torni a vedere il vero contenuto del form.

Tutto questo algoritmo è in realtà molto semplice, e si riduce a una decina di righe. Però ho pensato di creare una classe ad-hoc, in modo che il codice sia facilmente riutilizzabile, rilasci correttamente le risorse, e si protegga da utilizzi errati:

Public Class FormFreezer
  
Implements IDisposable

   ' The form being frozen
   Dim Form As Form
   ' the auxiliary PictureBox that will cover the form
   Dim PictureBox As PictureBox
   ' the number of times the Freeze method has been called
   Dim FreezeCount As Integer = 0

   ' create an instance associated with a given form
   ' and optionally freeze the form right away
   Public Sub New(ByVal form As Form, Optional ByVal freezeIt As Boolean = False)
      Me.Form = form
      If freezeIt Then Me.Freeze()
   End Sub

   ' freeze the form 
   Public Sub Freeze()
      ' Remember we have frozen the form once more
      FreezeCount += 1
      ' Do nothing if it was already frozen
      If FreezeCount > 1 Then Exit Sub

      ' Create a PictureBox that resizes with its contents
      PictureBox = New PictureBox()
      PictureBox.SizeMode = PictureBoxSizeMode.AutoSize
      ' create a bitmap as large as the form's client area and with same color depth
      Dim frmGraphics As Graphics = Form.CreateGraphics()
      Dim rect As Rectangle = Form.ClientRectangle
      PictureBox.Image = New Bitmap(rect.Width, rect.Height, frmGraphics)
      frmGraphics.Dispose()

      ' copy the screen contents, from the form's client area to the hidden bitmap
      Dim picGraphics As Graphics = Graphics.FromImage(PictureBox.Image)
      picGraphics.CopyFromScreen(Form.PointToScreen(New Point(rect.Left, rect.Top)), New Point(0, 0), New Size(rect.Width, rect.Height))
      picGraphics.Dispose()

      ' Display the bitmap in the picture box, and show the picture box in front of all other controls
      Form.Controls.Add(PictureBox)
      PictureBox.BringToFront()
   End Sub

   ' unfreeze the form
   ' Note: calls to Freeze and Unfreeze must be balanced, unless force=true 
   Public Sub Unfreeze(Optional ByVal force As Boolean = False)
      ' exit if nothing to unfreeze
      If FreezeCount = 0 Then Exit Sub
      ' remember we've unfrozen the form, but exit if it is still frozen
      FreezeCount -= 1
      ' force the unfreeze if so required
      If force Then FreezeCount = 0
      If FreezeCount > 0 Then Exit Sub

      ' remove the picture box control and clean up
      Form.Controls.Remove(PictureBox)
      PictureBox.Dispose()
      PictureBox = Nothing
   End Sub

   ' return true if the form is currently frozen
   Public ReadOnly Property IsFrozen() As Boolean
      Get
         Return FreezeCount > 0
      End Get
   End Property

   ' ensure that resources are cleaned up correctly
   Public Overridable Sub Dispose() Implements IDisposable.Dispose
      Me.Unfreeze(True)
   End Sub
End
Class

Questo è invece la versione C#, tradotta da Claudio:

public class FormFreezer: IDisposable
{

   // The form being frozen

   Form form;

   // the auxiliary PictureBox that will cover the form

   PictureBox pictureBox;

   // the number of times the Freeze method has been called

   int FreezeCount = 0;

 

   // create an instance associated with a given form

   // and freeze the form in base of flag freezeIt

   public FormFreezer(Form form, bool freezeIt)
   {

      this.form = form;

      if (freezeIt) this.Freeze();

   }

 

   // freeze the form 

   public void Freeze()
   {

      // Remember we have frozen the form once more

      // Do nothing if it was already frozen

      if (++FreezeCount > 1) 
         return;

      // Create a PictureBox that resizes with its contents

      pictureBox = new PictureBox();

      pictureBox.SizeMode = PictureBoxSizeMode.AutoSize;

      

      // create a bitmap as large as the form's client area and with same color depth

      Graphics frmGraphics = form.CreateGraphics();

      Rectangle rect = form.ClientRectangle;

      pictureBox.Image = new Bitmap(rect.Width, rect.Height, frmGraphics);

      frmGraphics.Dispose();

 

      // copy the screen contents, from the form's client area to the hidden bitmap

      Graphics picGraphics = Graphics.FromImage(pictureBox.Image);

      picGraphics.CopyFromScreen(form.PointToScreen(new Point(rect.Left, rect.Top)), new Point(0, 0), new Size(rect.Width, rect.Height));

      picGraphics.Dispose();

 

      // Display the bitmap in the picture box, and show the picture box in front of all other controls

      form.Controls.Add(pictureBox);

      pictureBox.BringToFront();

   }

 

   // unfreeze the form

   // Note: calls to Freeze and Unfreeze must be balanced, unless force=true

   public void Unfreeze(bool force)
   {

      // exit if nothing to unfreeze

      if ( FreezeCount == 0 ) 
         return ;

      // remember we've unfrozen the form, but exit if it is still frozen

      FreezeCount -= 1;

      // force the unfreeze if so required

      if (force) 
         FreezeCount = 0;

      if (FreezeCount > 0) 
         return;

      // remove the picture box control and clean up

      pictureBox.Controls.Remove(pictureBox);

      pictureBox.Dispose();

      pictureBox = null;

   }

 

   // return true if the form is currently frozen

   public bool IsFrozen
   {

      get { return (FreezeCount > 0); }

   }

 

   void IDisposable.Dispose()

   {

      this.Unfreeze(true);

   }

}

Usare la classe FormFreezer è semplicissimo. Ecco un esempio di codice (che deve trovarsi all'interno di una classe Form, in modo che Me punti al form corrente):

   Dim ff As New FormFreezer(Me, True)
   ' update controls here
  
' ...
  
ff.Unfreeze()

Poichè la classe implementa IDisposable, è possibile racchiudere il codice di aggiornamento in un blocco Using, sia in C# che in VB 2005, ed evitare la chiamata esplicita a Unfreeze:

   Using New FormFreezer(Me, True)
      ' Update controls here
     
' ...
  
End Using

Notate che le chiamate a Freeze e Unfreeze devono essere bilanciate, ad esempio se si eseguono due chiamate a Freeze occorreranno poi due chiamate a Unfreeze per "scongelare" effettivamente il form. Questo comportamento è simile a quello esposto dalle API di sistema che nascondono e mostrano il cursore del mouse, e funziona correttamente il form anche se il codice chiama Freeze e passa poi il controllo a un altro metodo che chiama anch'esso Freeze (a condizione pero' di usare la stessa istanza di FormFreezer).

5/3/2006 11:58:42 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Ho ripreso una macro che avevo scritto qualche anno fa: Essa impostava l'ordine di build dei progetti nella solution in base alle referenze tra i vari progetti.

VS.NET fa in automatico una cosa del genere quanod le referenze sono di tipo progetto, ma questo è un problema quando si devono gestire delle soluzioni che hanno un grado variabilità notevole in termini di progetti che la compongono.

Questo addin fa lo stesso mestiere per le referenze di tipo assembly...
La routine è stata aggiornata alla versione 2005 per gestire i progetti di tipo ASP.NET / WEB Service che rappresntano un tipo specifico di progetto.

 

Option Strict On
Imports System
Imports System.Windows.Forms
Imports EnvDTE
Imports EnvDTE80
Imports System.Diagnostics

Public Module CalcDepends

Public Function GetBuildOutPutWindow() As OutputWindowPane
Dim win As Window = DTE.Windows.Item( _
EnvDTE.Constants.vsWindowKindOutput)
Return CType(win.Object, OutputWindow).OutputWindowPanes.Item("Build")
End Function


Public Const c_prjtypeVBNET As String = "VBPROJ"
Public Const c_prjtypeCS As String = "CSPROJ"

Public Enum projectType
VBNET
CSHARP
UNKWOWN
End Enum
Public Function getProjectType(ByVal p_prj As Project) As projectType
Dim l_prjtype As String = p_prj.FullName.Substring(p_prj.FullName.Length - 6).ToUpper
If l_prjtype = c_prjtypeCS Then
Return projectType.CSHARP
ElseIf l_prjtype = c_prjtypeVBNET Then
Return projectType.VBNET
Else
Return projectType.UNKWOWN
End If

End Function

Public Sub CalcBuildDepends()
Try
GetBuildOutPutWindow.OutputString("++++++++++++++ ENTERING CalcBuildDepends ++++++++++++++" & vbCrLf)
Dim l_bd As BuildDependency
For Each l_bd In _
DTE.Solution.SolutionBuild.BuildDependencies
l_bd.RemoveAllProjects()
Next

For Each l_prac As EnvDTE.Project In _
DTE.Solution.Projects
If TypeOf l_prac.Object Is VSLangProj.VSProject Then
If getProjectType(l_prac) = projectType.CSHARP Or getProjectType(l_prac) = projectType.VBNET Then
Dim l_ActVSProject As VSLangProj.VSProject = CType(l_prac.Object, VSLangProj.VSProject)
' I get the Assembly Name of the Current Project
For Each ref As VSLangProj.Reference In l_ActVSProject.References
For Each l_pr As EnvDTE.Project In DTE.Solution.Projects
' looking for projects in the solution that are in the current project references list
If TypeOf l_pr.Object Is VSLangProj.VSProject Then
If getProjectType(l_pr) = projectType.CSHARP Or getProjectType(l_pr) = projectType.VBNET Then
Dim l_VSProject As VSLangProj.VSProject = CType(l_pr.Object, VSLangProj.VSProject)
'GetBuildOutPutWindow.OutputString("Checking if " _
' & l_ActVSProject.Project.Name _
' & " references " _
' & l_pr.Properties.Item("AssemblyName").Value.ToString & vbCrLf)

If (l_pr.Properties.Item("AssemblyName").Value.ToString = ref.Name) Then
Try
DTE.Solution.SolutionBuild.BuildDependencies.Item( _
l_ActVSProject.Project).AddProject(l_pr.UniqueName)
GetBuildOutPutWindow.OutputString("**** Adding Reference " & l_pr.Name & " to " & l_prac.Name + " *****" & vbCrLf)
System.Windows.Forms.Application.DoEvents()
Catch ex As System.Exception
MessageBox.Show("Error adding dependency " + l_pr.UniqueName + _
" to " + l_ActVSProject.Project.Name + " Error is:" + _
ex.Message)
End Try
End If
End If
End If
Next
Next
End If
ElseIf TypeOf l_prac.Object Is VsWebSite.VSWebSite Then
Dim l_ActVSProjectWS As VsWebSite.VSWebSite = CType(l_prac.Object, VsWebSite.VSWebSite)
For Each ref As VsWebSite.AssemblyReference In l_ActVSProjectWS.References
For Each l_pr As EnvDTE.Project In DTE.Solution.Projects
If TypeOf l_pr.Object Is VSLangProj.VSProject Then
If getProjectType(l_pr) = projectType.CSHARP Or getProjectType(l_pr) = projectType.VBNET Then
Dim l_VSProject As VSLangProj.VSProject = CType(l_pr.Object, VSLangProj.VSProject)
'GetBuildOutPutWindow.OutputString("Checking if " _
' & l_ActVSProject.Project.Name _
' & " references " _
' & l_pr.Properties.Item("AssemblyName").Value.ToString & vbCrLf)

If (l_pr.Properties.Item("AssemblyName").Value.ToString = ref.Name) Then
Try
DTE.Solution.SolutionBuild.BuildDependencies.Item( _
l_ActVSProjectWS.Project).AddProject(l_pr.UniqueName)
GetBuildOutPutWindow.OutputString("**** Adding Reference " & l_pr.Name & " to " & l_prac.Name + " *****" & vbCrLf)
System.Windows.Forms.Application.DoEvents()
Catch ex As System.Exception
MessageBox.Show("Error adding dependency " + l_pr.UniqueName + _
" to " + l_ActVSProjectWS.Project.Name + " Error is:" + _
ex.Message)
End Try
End If
End If
End If
Next
Next
End If
Next
GetBuildOutPutWindow.OutputString("++++++++++++++ EXITING CalcBuildDepends ++++++++++++++" & vbCrLf)
'MessageBox.Show("Recalculation Done")
Catch ex As System.Exception
MessageBox.Show("Error in seek:" + ex.Message)
End Try
End Sub

End Module

3/15/2006 5:05:56 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Sto riorganizzando la mia collection di MP3 e mi sono trovato nella necessità di rinominare un numero davvero enorme di file. Ci sono molte utility sul mercato che permettono di farlo (e che sono anche in grado di usare i tag MP3 per rinominare i file), ma ho pensato che tanto valeva scriverne una io. Tanto, grazie alle regex il compito non sarebbe stato difficile. E infatti in qualche minuto ho tirato fuori il seguente codice. Come vedete, la maggior parte delle istruzioni servono a estrarre e validare gli argomenti dalla riga di comando:

Imports System.Text.RegularExpressions
Imports System.IO

Module Renx

  
Function Main(ByVal args() As String) As Integer
     
Console.WriteLine("RENX (C) Francesco Balena / Code Architects Srl")

      Dim recurse As Boolean = False
     
Dim renameMode As Boolean = False
     
Dim oldNamePattern As String = Nothing
     
Dim newNamePattern As String = Nothing

      ' analyze each argument
     
For Each arg As String In args
        
Select Case arg.ToLower()
           
Case "/s", "-s"
              
recurse = True
            
Case "/r", "-r"
              
renameMode = True
           
Case "/h", "-h"
              
Return ShowHelp(0)
           
Case Else
              
If oldNamePattern Is Nothing Then
                 
oldNamePattern = "^" & arg & "$"
              
ElseIf newNamePattern Is Nothing Then
                 
newNamePattern = arg
              
Else
                 
Return ShowHelp(1)
              
End If
        
End Select
     
Next

      ' check that we have both mandatory arguments
     
If oldNamePattern Is Nothing OrElse newNamePattern Is Nothing Then
        
Return ShowHelp(1)
     
End If
     
' create the regex and check that pattern syntax is ok
     
Dim reSearch As Regex
     
Try
        
reSearch = New Regex(oldNamePattern, RegexOptions.IgnoreCase)
        
' test the replace pattern as well
        
Dim tmp As String = reSearch.Replace("a dummy string", newNamePattern)
      
Catch ex As Exception
         Console.WriteLine(
"SYNTAX ERROR: {0}", ex.Message)
        
Return 3
     
End Try
     
Console.WriteLine()

      ' iterate over all files in current directory (and its subdirectories, if recurse mode)
     
Dim searchOpt As SearchOption = SearchOption.TopDirectoryOnly
     
If recurse Then searchOpt = SearchOption.AllDirectories

      Dim parsedFilesCount As Integer = 0
     
Dim renamedFilesCount As Integer = 0
     
Dim errorsCount As Integer = 0
     
For Each oldFile As String In Directory.GetFiles(Directory.GetCurrentDirectory(), "*.*", searchOpt)
         parsedFilesCount += 1
        
' the regex applies to name only
        
Dim oldName As String = Path.GetFileName(oldFile)
        
Dim ma As Match = reSearch.Match(oldName)
        
If ma.Success Then
           
' this is the new name
           
Dim newName As String = ma.Result(newNamePattern)
            Console.WriteLine(oldFile)
            Console.Write(
" => {0}", newName)
            renamedFilesCount += 1
           
' proceed with rename only if not in simulation mode
           
If renameMode Then
              
Try
                 
Dim dirName As String = Path.GetDirectoryName(oldFile)
                 
Dim newFile As String = Path.Combine(dirName, newName)
                  File.Move(oldFile, newFile)
              
Catch ex As Exception
                  Console.Write(
" -- ERROR: {0}", ex.Message)
                  errorsCount += 1
              
End Try
           
End If
           
Console.WriteLine()
        
End If
     
Next

      ' Display a report
     
If renameMode Then
        
Console.WriteLine("Summary: {0} parsed files, {1} renamed files, {2} errors", parsedFilesCount, renamedFilesCount, errorsCount)
     
Else
        
Console.WriteLine("Summary: {0} parsed files, {1} files affected", parsedFilesCount, renamedFilesCount)
         Console.WriteLine()
         Console.WriteLine(
"NOTE: Running in simulation mode. Specify the /R option to actually rename files.")
     
End If
     
' Return an error code
     
If errorsCount = 0 Then
        
Return 0
     
Else
        
Return 2
     
End If
  
End Function

   Function ShowHelp(ByVal exitCode As Integer) As Integer
     
Console.WriteLine()
      Console.WriteLine(
"Syntax: RENX <oldnamepattern> <newnamepattern> [/R] [/S] [/H]")
      Console.WriteLine(
" oldnamepattern : regex that selects the files to be renamed")
      Console.WriteLine(
" newnamepattern : regex that specifies how files must be renamed")
      Console.WriteLine(
" /R : rename files")
      Console.WriteLine(
" /S : iterate over subdirectories")
      Console.WriteLine(
" /H : display this help")
      Console.WriteLine(
"NOTE: By default the program runs in simulation mode, and just displays how files would be renamed.")
      Console.WriteLine(
" You must specify the /R option to actually rename the files.")
     
Return exitCode
  
End Function

End Module

Al minimo, la utility RENX richiede due argomenti: una regex che individua quali file nella directory corrente (e nelle sue subdirectory, se si specifica l'opzione /S) devono essere rinominati, e una regex che specifica come rinominare i file che soddisfano la prima regex. La potenza di RENX sta nel fatto che la regex che individua i file può (anzi, deve) specificare uno o più gruppi di caratteri, che vengono poi riutilizzati nella seconda regex. Supponiamo ad esempio che abbia una directory con i seguenti file

        01 Speak to Me.mp3
        02 On the Run.mp3
        03 Time.mp3
        04 The Great Gig in the Sky.vbr
        05 Money.mp3
        06 Us and Them.mp3
        07 Any Colour You Like.vbr
        08 Brain Damage.mp3
        09 Eclipse.vb3

e li voglia rinominare in questo modo:

        01 - Speak to Me - The Dark Side of the Moon.mp3
        02 - On the Run - The Dark Side of the Moon.mp3
        03 - Time - The Dark Side of the Moon.mp3
        04 - The Great Gig in the Sky - The Dark Side of the Moon.vbr
        05 - Money - The Dark Side of the Moon.mp3
        06 - Us and Them - The Dark Side of the Moon.mp3
        07 - Any Colour You Like - The Dark Side of the Moon.vbr
        08 - Brain Damage - The Dark Side of the Moon.mp3
        09 - Eclipse - The Dark Side of the Moon.vbr

Ecco allora come invocare RENX per effettuare la trasformazione:

        RENX "(\d\d) (.+?)(\..+)"    "${1} - ${2} - The Dark Side of the Moon${3}"

Notate che la prima regex crea tre gruppi racchiudendo dei caratteri tra parentesi: (\d\d) fa il match con due cifre, mentre (.+?) fa il match con il nome del file, e infine (\..+) fa il match con l'estensione del file, punto incluso. Il secondo argomento puo' riordinare a piacere queste tre quantità, usando la sintassi ${N}, dove N è il numero d'ordine del gruppo individuato dalla prima regex, ed è quindi in grado di inserire un trattino dopo il numero del brano e il nome dell'album dopo il titolo del brano.

Data la pericolosità del comando di rename, per default il comando RENX *non* effettua il rename e si limita a mostrare come i file sarebbero rinominati. Per effettuare effettivamente il rename occorre specificare l'opzione /R:

        RENX "(\d\d) (.+?)(\..+)"    "${1} - ${2} - The Dark Side of the Moon${3}" /R

Questo è tutto. Avete il sorgente e potete estendere l'utility come preferite (magari per trasformarla in un programma Windows Forms), e potete anche effettuare il download della versione in binario da questo link: Renx.zip (5.51 KB)

3/6/2006 5:15:52 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Qualche minuto fa ho caricato tutti gli esempi a corredo di Programming Microsoft Visual Basic 2005: The Language sulla home page del libro. E' uno zippone di 7.5M, che contiene migliaia di righe di codice riutilizzabile, accuratamente scritte, testate e ottimizzate a manina. Tra le tante cose che potete trovare:

  • una classe VB che lavora con le frazioni
  • un form base per semplificare il data entry
  • una classe per il caching dei file di testo
  • alcuni custom iterator per scrivere dei For Each migliori
  • dozzine di esempi con i generics
  • due valutatori di espressioni, il primo basato sui regex, il secondo implementato tramite compilazione on-the-fly
  • una utlity per mostrare le statistiche di un progetto VB (è una demo per le regex)
  • RegexTester, una utility stand-alone per aiutarvi a costruire, testare, e compilare le regular expression
  • un esempio di custom provider per My.Settings
  • un esempio di come intercettare qualsiasi evento da qualsiasi controllo su un form
  • benchmark basati su attributi custom
  • una completa infrastruttura per scrivere applicazioni Windows Forms estendibili via plug-in
  • attributi e DLL per scrivere applicazioni N-tier data-centriche
  • numerose macro per Visual Studio
  • visualizer per mostrare il contenuto di file, regex, e immagini durante il debug
  • .... e molto altro.

Sulla stessa pagina ho caricato un document di errata per gli errori trovati dopo che il libro è andato in stampa, e alcuni capitoli di esempio. Per accedere alla pagina occorre registrarsi.

Divertitevi con il codice ( ... e comprate il libro se lo trovate interessante! :-) )

2/28/2006 7:25:51 PM (GMT Standard Time, UTC+00:00) #  | Comments [2] | 

Più lavoro con i generics più mi piacciano e più modi scopro per semplificare il mio codice mediante il loro utilizzo. In particolare, mi piace la possibilità di scrivere codice type-safe, più conciso ed efficiente ma soprattutto più leggibile perchè non contiene dozzine di CType e DirectCast. Oggi ho raccolto in questo modulo le funzioni generiche che uso più spesso nei miei programmi. Sono molto brevi e semplici, ma mi hanno fatto risparmiare un bel po' di tempo e codice.

Module GenericFunctions

   ' Swap two variables
  
Public Sub Swap(Of T)(ByRef var1 As T, ByRef var2 As T)
     
Dim tmp As T = var1
      var1 = var2
      var2 = tmp
  
End Sub

   ' Type-safe version of the IIF function
  
' returns valueOnTrue if expression is True, else returns valueOnFalse
  
Public Function IIf(Of T)(ByVal expression As Boolean, ByVal valueOnTrue As T, ByVal valueOnFalse As T) As T
     
If expression Then
        
Return valueOnTrue
     
Else
        
Return valueOnFalse
     
End If
  
End Function

   ' Type-safe version of the Choose function
  
' returns the N-th element of a list of values, or the default value for T if index
  
' is less than 0 or higher than the number of values
  
Public Function Choose(Of T)(ByVal index As Integer, ByVal values() As T) As T
     
If index >= 0 AndAlso index < values.Length Then
        
Return values(index)
     
Else
        
Return Nothing
     
End If
  
End Function

   ' Return an array of the specified type
  
Public Function NewArray(Of T)(ByVal ParamArray values() As T) As T()
     
Return values
  
End Function

   ' Return the min value of a list
  
Public Function Min(Of T As IComparable)(ByVal firstValue As T, ByVal ParamArray values() As T) As T
     
Dim result As T = firstValue
     
For Each value As T In values
        
If result.CompareTo(value) > 0 Then result = value
     
Next
     
Return result
  
End Function

   ' Return the max value of a list
  
Public Function Max(Of T As IComparable)(ByVal firstValue As T, ByVal ParamArray values() As T) As T
     
Dim result As T = firstValue
     
For Each value As T In values
        
If result.CompareTo(value) < 0 Then result = value
     
Next
     
Return result
  
End Function

   ' Return True if a value is in specific range
  
Public Function InRange(Of T As IComparable)(ByVal testValue As T, ByVal minValue As T, ByVal maxValue As T) As Boolean
     
Return testValue.CompareTo(minValue) >= 0 AndAlso testValue.CompareTo(maxValue) <= 0
  
End Function

   ' Retrieve a dictionary element of a given type, or the provided default value if the element isn't found
  
' (two overloads)
  
Public Function GetDictionaryValue(Of TKey, TValue)(ByVal dict As Hashtable, ByVal key As TKey, ByVal defaultValue As TValue) As TValue
     
If dict.ContainsKey(key) Then
        
Return CType(dict(key), TValue)
     
Else
        
Return defaultValue
     
End If
  
End Function

   Public Function GetDictionaryValue(Of TKey, TValue)(ByVal dict As Dictionary(Of TKey, TValue), ByVal key As TKey, ByVal defaultValue As TValue) As TValue
     
' If the key is in the dictionary, the following statement stores the corresponding value
     
' in defaultValue, else it leave defaultValue unchanged
     
dict.TryGetValue(key, defaultValue)
     
Return defaultValue
  
End Function

End Module

L'uso della maggior parte dei metodi dovrebbe essere evidente. Una delle funzioni più utili è NewArray, in quanto permette di creare al volo un array da passare a un metodo. Supponiamo ad esempio che il metodo DoSomething richieda un vettore di interi. Ecco quali sono le opzioni a disposizione in VB2005:

    ' 1. Create the array first, than pass it
    Dim values() As Integer = {1, 2, 3, 4, 5}
    DoSomething(values)

    ' 2. Create the array on the fly using the nearly-undocumented syntax
    DoSomething(New Integer() {1, 2, 3, 4, 5})

Io di solito uso la seconda sintassi, ma ho notato che pochi programmatori VB la conoscono. Il codice diventa molto più pulito e leggibile con la funzione NewArray:

    ' 3. Use the NewArray generic function to create the array on the fly
    DoSomething(NewArray(1, 2, 3, 4, 5))

Il metodo NewArray si dimostra molto utile anche per creare dei cicli For il cui indice varia a piacere.

    ' Test whether "number" is a prime number in the range 1-1000
    For Each n As Integer In NewArray(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31)
       If (number Mod n) = 0 Then Console.Write("{0} is not prime", number): Exit For
    Next

2/23/2006 8:00:38 AM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

In VB6 - e più in generale, nel mondo COM - un oggetto si distrugge semplicemente impostandolo a Nothing (ovviamente, nell'ipotesi in cui non vi siano altre variabili che puntano a quella particolare istanza). Il runtime di VB invoca il metodo Class_Terminate, se esiste un handler per tale evento, poi dealloca tutte le risorse assegnate all'oggetto, inclusa la mamoria. Questo processo è ricorsivo, quindi se l'oggetto possiede altri oggetti COM (ad es. una connessione ADO), anche quegli oggetti sono distrutti.

Sappiamo bene, invece, che in .NET impostare una variabile oggetto a Nothing non fa scattare alcun evento. Se l'oggetto implementa il metodo Finalize, tale metodo sarà chiamato dal garbage collector di .NET solo qualche tempo dopo. L'intervallo di tempo tra l'impostazione a Nothing e l'esecuzione del codice di cleanup contentuo nel metodo Finalize dipende da tanti fattori, e potrebbe arrivare anche a minuti o ore, se il programma non alloca molti altri oggetti e quindi non stressa il garbage collector.

Questa differenza nel comportamento tra VB6 e VB.NET diventa spesso un problema davvero serio nella migrazione delle applicazioni. Se ad esempio il codice nell'evento Class_Terminate chiude una connessione al database, oppure un file o una porta seriale, oppure cancella delle informazioni confidenziali, o chiude un form, allora il ritardo tra la distruzione "logica" dell'oggetto e la sua distruzione "fisica" può compromettere il funzionamento della applicazione stesso, dopo la sua migrazione verso il mondo .NET.

Purtroppo non è possibile ottenere "in automatico" che VB.NET si comporti esattamente come VB6 in questo caso, pero' possiamo fare qualcosa che riduce il problema. Questo è possibile, ancora una volta, grazie ai generics. Basta inserire il metodo seguente in un modulo, in modo da renderlo visibile a tutta l'applicazione:

Public Sub SetNothing(Of T)(ByRef obj As T)
  
' Dispose of the object if possible
  
If obj IsNot Nothing AndAlso TypeOf obj Is IDisposable Then
     
DirectCast(obj, IDisposable).Dispose()
  
End If
  
' Decrease the reference counter, if it's a COM object
  
If Marshal.IsComObject(obj) Then
     
Marshal.ReleaseComObject(obj)
  
End If
  
obj = Nothing
End Sub

A questo punto, possiamo fare un "search and replace" in tutto il codice VB.NET prodotto dal wizard di migrazione, sostituendo le istruzioni var = Nothing con SetNothing.(var). Ovviamente, questo tecnica non risolve tutti i problemi menzionati in precedenza, perchè funziona solo con le variabili esplicitamente messe a Nothing via codice, e non quelle che sono azzerato implicitamente quando escono dallo scope corrente.

Poichè si tratta di una sostituzione con parti variabili, sembrerebbe che non sia possibile farlo in modo davvero automatico, e invece non è così, perchè possiamo usare le feature di ricerca e sostituzione di Visual Studio basate sulle regular expression. (Per maggiori info, vedere questo post.) Infatti, basta attivare la ricerca per regular expression, inserire la stringa <{:i} = Nothing come stringa da ricercare, e la stringa SetNothing(\1) come stringa di sostituzione. Et voilà :-)

NOTA per i refrattari alle regex: la prima stringa dice di ricercare una variabile (:i) che si trova all'inizio di parola (<); le graffe in {:i} eseguono il tagging del nome dell'identificatore, in modo da poter poi usare il nome della variabile nel testo di sostituzione (mediante il placeholder \1). Tutto qui, non era mica difficile.

 

2/14/2006 12:55:33 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Il tipo Date di VB6 e il tipo Date di VB.NET (o il tipo System.DateTime di .NET, il che è lo stesso) sono molto simili, eccetto che per un dettaglio importante: in VB6 le date sono conservate internamente con valori Double, la cui parte intera rappresenta il numero di giorni trascorsi dal 30-12-1899 (non chiedetemi perchè non dal 31 dicembre...), e la cui parte decimale rappresenta le ore, minuti, e secondi sotto forma di frazione di giorno.

Il wizard che migra le applicazioni VB6 in VB.NET non modifica in alcun modo le variabili di tipo Date però deve tenere conto del fatto che i programma VB6 eseguono spesso operazioni con le date sfruttando il formato di memorizzazione interno. Ad esempio, ecco come in VB6 si calcola la data corrispondente a sette giorni dalla data odierna:

    Dim da As Date
    da = Now + 7
    MsgBox da

Per migrare correttamente le operazioni aritmetiche sulle date, il wizard usa i metodi FromOADate e ToOADate della classe Date, in questo modo:

   Dim da As Date
   da = Now
   da = System.Date.FromOADate(da.ToOADate + 7)

Non è un bel codice a vedersi, ma funziona. Anche in questo caso, però, è possibile applicare qualche trucchetto per ridurre al minimo le differenze sintattiche tra il codice VB6 e VB.NET e supportare l'aritmetica tra le date anche in VB.NET senza inserire nuovi metodi. Il meccanismo si basa sulla definizione di un nuovo tipo VB6Date, che conserva internamente un valore Date, che supporta le conversioni implicite da e verso i tipi Date, String, e Double, e che supporta anche gli operatori + e - con interi e Double:

<DebuggerDisplay("{DateValue}")> _
Public Structure VB6Date

   ' the origin of all dates
  
Private Shared ReadOnly StartDate As Date = New Date(1899, 12, 30)

   ' this is where the actual date value is stored
  
Private DateValue As Date

   ' private constructor (Friend to be visible from the VB6DateFunctions module)
  
Friend Sub New(ByVal value As Date)
     
Me.DateValue = value
   End Sub

   ' override the ToString method, to support Console.Write, etc.
  
Public Overrides Function ToString() As String
     
Return DateValue.ToString()
  
End Function

   ' convert to/from Date values 
  
Public Shared Widening Operator CType(ByVal value As VB6Date) As Date
     
Return value.DateValue
  
End Operator

   Public Shared Widening Operator CType(ByVal value As Date) As VB6Date
     
Return New VB6Date(value)
  
End Operator

   ' convert to/from Integer values 
  
Public Shared Widening Operator CType(ByVal value As VB6Date) As Integer
     
Return value.DateValue.Subtract(StartDate).Days
  
End Operator

   Public Shared Widening Operator CType(ByVal value As Integer) As VB6Date
     
Return New VB6Date(StartDate.Add(New TimeSpan(value, 0, 0, 0)))
  
End Operator

   ' convert to/from Double values 
  
Public Shared Widening Operator CType(ByVal value As VB6Date) As Double
     
Return value.DateValue.Subtract(StartDate).Ticks / 864000000000
  
End Operator

   Public Shared Widening Operator CType(ByVal value As Double) As VB6Date
     
Return New VB6Date(StartDate.Add(New TimeSpan(CLng(value * 864000000000))))
  
End Operator

   ' convert to/from strings 
  
Public Shared Widening Operator CType(ByVal value As VB6Date) As String
     
Return value.ToString()
  
End Operator

   Public Shared Widening Operator CType(ByVal value As String) As VB6Date
     
Return New VB6Date(Date.Parse(value))
  
End Operator

   ' the + and - operators, with Integer and Double values
  
Public Shared Operator +(ByVal value As VB6Date, ByVal days As Integer) As VB6Date
     
Return New VB6Date(value.DateValue.Add(New TimeSpan(days, 0, 0, 0)))
  
End Operator

   Public Shared Operator -(ByVal value As VB6Date, ByVal days As Integer) As VB6Date
     
Return New VB6Date(value.DateValue.Subtract(New TimeSpan(days, 0, 0, 0)))
  
End Operator

   Public Shared Operator +(ByVal value As VB6Date, ByVal days As Double) As VB6Date
     
Return New VB6Date(value.DateValue.Add(New TimeSpan(CLng(days * 864000000000))))
  
End Operator

   Public Shared Operator -(ByVal value As VB6Date, ByVal days As Double) As VB6Date
     
Return New VB6Date(value.DateValue.Subtract(New TimeSpan(CLng(days * 864000000000))))
  
End Operator

End Structure

Notate l'attributo DebuggerDisplay, che permette di visualizzare la data contenuta in un oggetto VB6Date quando si sposta il cursore del mouse sul nome di una variabile (solo in Visual Studio 2005).

Per completare l'effetto, occorre scrivere anche dei rimpiazzi per le funzioni Now, Date (ovvero Today in VB.NET), e Time, in modo che anche queste funzioni restituiscano un oggetto di tipo VB6Date:

Public Module VB6DateFunctions
  
Public ReadOnly Property Now() As VB6Date
     
Get
        
Return New VB6Date(Microsoft.VisualBasic.DateAndTime.Now)
     
End Get
   
End Property

   Public ReadOnly Property [Today]() As VB6Date
     
Get
        
Return New VB6Date(Microsoft.VisualBasic.DateAndTime.Today)
     
End Get
  
End Property

   Public ReadOnly Property Time() As VB6Date
     
Get
        
Return New VB6Date(Now - Today)
     
End Get
  
End Property
End
Module

A questo punto è possibile migrare il codice VB6 contenente operazioni aritmetiche sulle date semplicemente sostituendo il tipo della variabile che ospita tali valori:

' VB.NET code
Dim da As VB6Date    ' <-- the only modified statement
da = Now + 7
MsgBox(da)

Con questo esempio non sto certo suggerendo di cambiare TUTTE le vostre variabili Date per usare il tipo custom VB6Date. Il mio obiettivo principale è soprattutto di mostrare come - con un po' di fantasia e soprattutto sfruttando le nuove feature di VB2005, in questo caso l'overloading degli operatori - sia spesso possibile ridurre la distanza tra due linguaggi simili ma subdolamente differenti come VB6 e VB.NET. Ovviamente il discorso potrebbe valere anche nella migrazione in .NET da altri linguaggi ancora (ad esempio Java).


In chiusura, mi piacerebbe fare un piccolo sondaggio:

1) quanti di voi sviluppano o anche solo manutengono applicazioni VB6 ?
2) se si, perchè l'applicazione non è stata ancora convertita in VB.NET ?
3) in generale, quali ritenete siano gli ostacoli principali alla conversione ?
4) e quali sono i principali pregi e difetti del wizard di migrazione fornito con Visual Studio ?

2/9/2006 12:51:24 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Mentre cominiciavo a prendere confidenza con (W)WF, cioè Windows WorkFlow Foundation, mi sono scontrato con un baco della versine corrente (in beta), per cui non sono sollevati gli eventi di passaggio di stato di un'Activity.

Per girare attorno al problema avevo la necessità di far si che la mia classe che deriva da SequenceActivity andasse in override del metodo OnEvent.

Qui sorge però il problema: tale metodo non è overridable: esso è definito in un'interfaccia implementata dalla classe base in maniera esplicita.

Se vengo a reimplementare l'interfaccia esplicitamente l compilatore C# non da errore ed il runtime del WF chiama la mia implementazione .. ma come faccio a chiamare l'implementazione esplicita della classe base, cioè come faccio a fare logicamente base.OnEvent ?

Dopo un breve consultazione con Francesco, siamo giunti alla conclusione che pare non esista nessuna keyowrd o sintassi del linguaggio che possa aiutarci. Occorre lavorare di reflection come mostrato nel codice sottostante.

Si noti che si chiama GetMethod su BaseType this.GetType().BaseType

// My Interface explicit re-implementation
void IActivityEventListener<ActivityExecutionStatusChangedEventArgs>.OnEvent(
   object sender, ActivityExecutionStatusChangedEventArgs e) {

// I do my work here ...

// the following code is logically like calling base.OnEvent
string
methodName = "System.Workflow.ComponentModel.IActivityEventListener<System.Workflow.ComponentModel.ActivityExecutionStatusChangedEventArgs>.OnEvent";
MethodInfo myMethod = this.GetType().BaseType.GetMethod(methodName,BindingFlags.Instance | BindingFlags.NonPublic);
myMethod.Invoke(this, new object[] { sender, e });
}

Notate che per ragioni di performance, se occorre chiamare il metodo più di una volta, è opportuno acquisire una referenza a myMethod una sola volta e poi tenere tale referenza in cache per i successivi riutilizzi. 
2/8/2006 10:32:07 AM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Continuano le mie ricerche su come tradurre in modo indolore da VB6 a VB.NET, cercando di superare i molti limiti del wizard di migrazione incluso in Visual Studio. Il wizard in questione, che si comporta egregiamente quando deve convertire alcuni costrutti particolari (un esempio per tutto: On ... Goto), si arrende invece davanti ad alcune banalità che potrebbero essere superate facilmente con un po' di fantasia.

Per esempio, in VB.NET manca la funzione IsMissing (da cui il titolo sibillino di questo post...). Come i VB-isti ricorderanno sicuramente, questa funzione permette di capire se il chiamante ha effettivamente omesso un argomento passato a un parametro Optional. Ecco ad esempio un esempio di codice VB6, peraltro abbastanza comune, che il wizard non riesce a convertire correttamente:

Sub DoSomething(Optional ByVal value As Variant)
    If IsMissing(value) Then value = Now
    ' ...
End Sub

I Variant sono convertiti in Object e in VB.NET è obbligatorio specificare il valore di default di un argomento opzionale, ma poichè non esiste un equivalente del valore "missing" in VB.NET, il wizard non sa bene come tradurre la funzione IsMissing. Eppure la soluzione è davvero semplice: basta definire una constante stringa "improbabile", usare tale stringa come valore di default per i valori opzionali, e scrivere poi una funzione IsMissing personalizzata che confronta il suo argomento con tale stringa. Ecco un esempio :

Public Module Functions

   ' The MissingValue6 constant (a very unusual string)
  
Public Const MissingValue6 As String = ControlChars.VerticalTab & ControlChars.FormFeed & "MissingValue" & ControlChars.VerticalTab

   ' The IsMissing function
  
Public Function IsMissing(ByVal value As Object) As Boolean
     
Return MissingValue6.Equals(value)
  
End Function

End Module

A questo punto il codice VB6 in questione puo' essere tradotto quasi alla lettera:

' this is VB.NET code 
Sub DoSomething(Optional ByVal value As Object = MissingValue6 )
    If IsMissing(value) Then value = Now
    ' ...
End Sub

Certo, è uno "sporco trucco", ma sempre meglio che dover rivedere tutte le procedure VB6 che usano argomenti opzionali. Non era difficile implementare qualcosa del genere, bastava pensarci, no?

2/8/2006 9:31:57 AM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Quasi non ci speravo più, ma alla fine qualcuno è riuscito a risolvere il Quizzettone #9, ovvero come determinare una mano di poker in solo otto istruzioni. Anzi, il bravissimo Matteo Conta è riuscito a farlo in solo sette istruzioni. Graaaaande! Trovate la sua soluzione in coda ai vari commenti al post.

Come promesso, passo invece a illustrare la mia soluzione in otto istruzioni. La soluzione è in VB2005 (tratta dal nuovo libro Programming Microsoft Visual Basic 2005: The Language), ma potrebbe essere facilmente resa anche in C#:

Public Shared Function EvalPokerScore2(ByVal ParamArray cards() As String) As String
  
' Sort the array and create the sequence of values and of suits.
  
Array.Sort(cards)
  
Dim values As String = cards(0)(0) + cards(1)(0) + cards(2)(0) + cards(3)(0) + cards(4)(0)
  
Dim suits As String = cards(0)(1) + cards(1)(1) + cards(2)(1) + cards(3)(1) + cards(4)(1)

   Dim scores(,) As String = { _
      {
"12345|23456|34567|45678|56789|6789T|789JT|89JQT|9JKQT|1JKQT", "(.)\1\1\1\1", "StraightFlush"}, _
      {
"(.)\1\1\1", ".", "FourOfAKind"}, _
      {
"(.)\1\1(.)\2|(.)\3(.)\4\4", ".", "FullHouse"}, _
      {
".", "(.)\1\1\1\1", "Flush"}, _
      {
"12345|23456|34567|45678|56789|6789T|789JT|89JQT|9JKQT|1JKQT", ".", "Straight"}, _
      {
"(.)\1\1", ".", "ThreeOfAKind"}, _
      {
"(.)\1.?(.)\2", ".", "TwoPairs"}, _
      {
"(.)\1", ".", "OnePair"}}
  
For i As Integer = 0 To scores.GetUpperBound(0)
     
If Regex.IsMatch(values, scores(i, 0)) AndAlso Regex.IsMatch(suits, scores(i, 1)) Then
        
Return scores(i, 2)
     
End If
  
Next
  
Return "HighCard"
End Function

(Nel computo delle istruzioni, non si conta nè End If nè Next, secondo le regole enunciate nel quiz...)

Per prima cosa il programma crea due stringhe, values contentente la sequenza dei valori delle carte, suits contenente la sequenza dei semi, poi definisce il vettore bidimensionale scores con le informazioni per cercare i vari punteggi. Ogni riga del vettore scores contiene due regular expression - una che viene applicata alla stringa values, l'altra alla stringa suits. Se entrambe le regex sono soddisfatte, il programma restituisce il terzo elemento della riga, ovvero il nome della combinazione. Notate che la seconda regex (quella che testa i colori) viene usata solo in due casi, per StraightFlush (scala reale) e Flush (colore), quindi in tutti gli altri casi basta usare la regex "." (qualsiasi carattere).

La maggior parte delle regex che testano i valori usano le cosiddette "backreference", ad es. "(.)\1\1" significa "cerca un carattere qualsiasi (il punto) poi ricercalo altre due volte (\1\1): il risultato della ricerca è positivo se vi sono tre carte uguali una dopo l'altra. Notare che l'array cards è ordinato rispetto al valore delle carte, quindi abbiamo la sicurezza che in values le carte con lo stesso valore sono adiacenti.

Proprio perche la stringa values è ordinata per valori, le regex da usare per testare le scale (StraightFlush e Straight) contengono dei caratteri apparentemente fuori ordine. Ad esempio, la scala massima si testa con la regex "1JKQT" e non con sequenza più intuitiva (ma errata), "TJQK1".

Concludendo, anche se la soluzione di Matteo è a dir poco sbalorditiva e assolutamente geniale, direi che quella basata su regex è decisamente più leggibile e manutenibile.


NOTA: Questa soluzione è in 8 istruzioni, ma ho spiegato che esiste anche la possibilità di scriverla in sole sette istruzioni. Questo si ottiene evitando di dividere le carte in due stringhe distinte (values e suits), ed usando invece una sola stringa data dalla concatenazione delle coppie (valore,seme). Anche in questo caso è possibile scrivere delle regex che trovano correttamaente tutte le combinazioni, ma le regex stesse sono molto più complesse, quindi ho deciso che nel libro avrei pubblicato questa versione più lunga ma più leggibile.

In realtà è persino possibile modificare il precedente codice per ottenere una soluzione in sole sei istruzioni: infatti, invece di creare le stringhe values e suits fuori dal ciclo, potrei crearle al volo ad ogni iterazione del ciclo For..Next. Non solo, ma se aggiungo una ulteriore riga alla matrice scores, contenente i sequenti valori:

         {".", ".", "HighCard"}}

Allora posso evitare anche l'istruzione Return finale, portando il totale a sole cinque istruzioni, anche se in quest'ultimo caso devo tenermi il warning che VB emette perchè non tutti i possibili percorsi della funzione restituiscono un valore. Insomma, se davvero volete scrivere codice illeggibile, non c'è che l'imbarazzo della scelta :-)

Ultimissima nota: un pubblico ringraziamento e una pubblica lode anche a Paolo Possanzini, per essere stato il primo ad accanirsi e a risolvere, qualche mese fa mentre scrivevo il libro, questo quiz. Onore al merito!

2/3/2006 4:53:39 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

In questo periodo mi sto interessando molto ai problemi di migrazione delle applicazioni VB6 verso VB.NET. Personalmente non ho realmente "migrato" nessuna delle mie applicazioni, perchè ho sempre preferito riscriverle da zero per sfruttare al massimo le feature di .NET e soprattutto perchè il wizard di conversione fa un lavoro oggettivamente penoso, producendo un codice brutto e non manutenibile. O meglio, il wizard fa un lavoro decente, ma il vero problema sono le incompatiblità tra VB6 e VB.NET.

Alcune di queste incompatibilità, tuttavia, possono essere superate con un po' di fantasia, soprattutto ora che VB2005 offre delle feature che prima non erano disponibili. Prendiamo ad esempio il problema "classico" degli array con indice diverso da zero, che hanno fatto dannare tutti gli sviluppatori VB6 alle prese con il porting verso VB.NET. Supponiamo di avere il seguente codice VB6

      Dim arr(1 to 10) as Integer
      Dim i As Integer, prod As Integer, v As Variant
      For i = LBound(arr) To UBound(arr)
         arr(i) = i
      Next
      For Each v in arr
         prod = prod * v
      Next

Il wizard di conversione sostituirà l'indice iniziale 1 con l'indice 0, quindi l'array avrà un elemento in più. E' evidente che alla fine dell'esecuzione il valore di prod sarà zero, mentre avrebbe dovuto pari al fattoriale di 10!. Si tratta di errori molto subdoli, che costringono in pratica a ristudiare attentamente tutto il codice e a testare da zero l'applicazione. L'approccio manuale migliore è sicuramente quello di modificare la dichiarazione per "scalare" l'array in modo che il suo primo elemento reale abbia indice zero, e poi modificare TUTTI i riferimenti agli elementi dell'array, per tenere conto dello shift:

      Dim arr(0 to 10-1) as Integer     
      Dim i As Integer, prod As Integer
      For i = LBound(arr) To UBound(arr)
         arr(i - 1)  = i
      Next

Ma anche questo approccio richiede molta attenzione e in certi casi non puo' essere usato, ad esempio quando l'array viene passato a una procedura che deve funzionare con array di qualunque tipo (e il cui codice non sa che deve scalare l'indice); in altri casi ancora, il programma potrebbe prendere delle decisioni in base al valore di LBound e UBound, che nella nuova versione è modificato.

La domanda che mi sono posto è: è possibile convertire quel codice in VB2005 senza doversi preoccupare di tutti questi problemi? La soluzione è stata relativamente semplice, grazie ai generics e a qualche trucchetto con l'ereditarietà.

' Base class

Public Class VBArrayBase
  
Protected Friend lowerIndex As Integer
  
Protected Friend upperIndex As Integer
End Class

' One dimensional array of type T

Public Class VBArray(Of T)
  
Inherits VBArrayBase
  
Implements IEnumerable

   Dim items() As T

   Sub New(ByVal lowerIndex As Integer, ByVal upperIndex As Integer)
     
Me.lowerIndex = lowerIndex
     
Me.upperIndex = upperIndex
     
ReDim items(upperIndex - lowerIndex)
  
End Sub

   Default Property Item(ByVal index As Integer) As T
     
Get
        
Return items(index - lowerIndex)
     
End Get
     
Set(ByVal value As T)
         items(index - lowerIndex) = value
     
End Set
  
End Property

   Public Function GetEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
     
Return items.GetEnumerator()
  
End Function

End Class

Notate con quanta semplicità è possibile supportare i cicli For Each: basta restituire l'oggetto IEnumerator dell'array interno. Occorre a questo punto estendere le istruzioni LBound e UBound alla nuova classe VBArray. Per fare ciò dovete creare un modulo pubblico a parte, con queste istruzioni:

Public Module ArrayFunctionsVB6
   Function LBound(ByVal arr As Array, Optional ByVal rank As Integer = 1) As Integer
     
Return Microsoft.VisualBasic.Information.LBound(arr, rank)
  
End Function

   Function UBound(ByVal arr As Array, Optional ByVal rank As Integer = 1) As Integer
     
Return Microsoft.VisualBasic.Information.LBound(arr, rank)
  
End Function

   Function LBound(ByVal arr As VBArrayBase, Optional ByVal rank As Integer = 1) As Integer
     
If rank = 1 Then
        
Return arr.lowerIndex
     
Else
        
Throw New IndexOutOfRangeException()
     
End If
  
End Function

   Function UBound(ByVal arr As VBArrayBase, Optional ByVal rank As Integer = 1) As Integer
     
If rank = 1 Then
        
Return arr.upperIndex
     
Else
        
Throw New IndexOutOfRangeException()
     
End If
  
End Function
End
Module

Notate che il modulo deve contenere due overload per LBound e UBound, uno per gli array standard e uno per i nostri nuovi array. Purtroppo non è possibile avere un progetto che referenzia due moduli differenti (uno nella libreria di compatibiltà di VB e uno in una nostra DLL) e prenda due overload dello stesso metodo da entrambi i moduli. Se cio' avviene solo uno dei due metodi è visto dal programma principale.

Altro dettaglio interessante: Il codice dei metodi LBound e UBound ha bisogno di accedere ai membri Friend della classe VBArray(Of T), ma non è possible usare il tipo VBArray(Of T) nella dichiarazione di questi metodi. Ecco perchè ho dovuto definire una classe base VBArrayBase e fare ereditare VBArray(Of T) da quella classe base.

Grazie alla classe VBArray(Of T) e al modulo ArrayFunctionsVB6, possiamo convertire il codice VB6 cambiando soltanto la dichiarazione del vettore:

     Dim arr As New VBArray(Of Short)(1, 10)      ' Short invece che Integer

Tutto il resto del programma funzionarà esattamente come in VB6, incluso il ciclo For Each e le chiamate a LBound/UBound. Provare per credere! :-)

1/29/2006 2:44:49 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Che settimana!

Sono tornato a casa venerdi sera, dopo cinque giorni pieni di WPC. Ero cosi cotto che ho dormito in aereo tutto il tempo, ma ovviamente ne valeva la pena. Nonostante qualche problema tecnico - dovuto al fatto che qualche giorno prima della conf il mio Dell mi ha abbandonato e ho dovuto portare tutto su un altro notebook - le mie sessioni sono andate bene, come pure tutte quelle del team di CA.

Stamattina finalmente (per modo di dire) si torna al lavoro. La prima cosa da fare è mandare i sorgenti delle demo allo staff della WPC, per farle mettere online. Provo a zippare il tutto e arrivo a 7-8 mega di roba. Il motivo è che nello zippone sono inclusi tutti i file prodotti dalla compilazione (exe, dll, obj, ecc.), di cui si puo' fare a meno quando si mandano i sorgenti. E' la solita storia, che si ripete ogni volta che devo mandare il codice per un articolo, una conferenza, o anche semplicemente quando devo passarlo a un amico via email.

Questa volta, però, invece di rimuovere manualmente ogni file, decido di investire 2 minuti (due minuti davvero, non è un modo di dire), per scrivere una piccola utility command line che fa il lavoro per me. Grazie a un overload di Directory.GetDirectories aggiunto a .NET 2.0, è possibile ottenere in un sol colpo tutte le directory in un albero di directory, quindi si tratta semplicemente di cancellare tutti i folder di nome "obj" e "bin". Se la cancellazione fallisce viene mostrato un messaggio di errore: questo puo' accadere se un eseguibile è in esecuzione e non può essere cancellato.

Imports System.IO

Module Module1
  
Sub Main(ByVal args() As String)
      ' Use current directory if no argument has been specified
     
Dim rootDir As String = Directory.GetCurrentDirectory()
     
If args.Length > 0 Then rootDir = args(0)
     
' Read all the folder names in the specified directory tree
     
Dim dirNames() As String = Directory.GetDirectories(rootDir, "*.*", SearchOption.AllDirectories)
     
Dim errors As Integer = 0

      ' Delete all the BIN and OBJ subdirectories
     
For Each dir As String In dirNames
        
Dim dirName As String = Path.GetFileName(dir).ToLower()
           
If dirName = "bin" OrElse dirName = "obj" Then
              
Try
                 
Console.Write("Deleting {0} ...", dir)
                  Directory.Delete(dir,
True)
                  Console.WriteLine(
"DONE")
              
Catch ex As Exception
                  Console.WriteLine()
                  Console.WriteLine(
" ERROR: {0}", ex.Message)
                  errors += 1
              
End Try
           
End If
        
Next

         Console.WriteLine()
        
If errors = 0 Then
           
Console.WriteLine("All directories were removed successfully")
        
Else
           
Console.WriteLine("{0} directories couldn't be removed", errors )
        
End If
    
End Sub
End
Module

Oltre ad essere usata dalla riga di comando, potete aggiungere questa utility al menu Tools di Visual Studio, per permettere di cancellare tutti i file prodotti dalla compilazione della soluzione corrente, usando il seguente comando

               DELETEBINPATH $(SolutionDir)

dove ovviamente si suppone che DeleteBinPath sia il nome con cui avete compilato la utility. 

11/21/2005 8:08:36 AM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Vi sarà capitato di vedere, in del codice di esempio o generato da Wizards, l'utilizzo del motodo GetChanges del Dataset per estrarre un Dataset con solo le righe modificate. Tale Datsaet veniva mandato al Business Layer potenzialment remoto invece che tutto il DataSet per risparmiare banda.

Evidentemente questa cosa non mi ha mai convinto: se il mio Dataset era fatto da una Table Ordine e una Table RigaOrdine, mi sarei ritrovato sul server degli "oggetti" ordine incompleti .. come avrei potuto implementare logiche che richiedevano per esempio un numero minimo di righe nell'ordine ?

Quando uno usa i Dataset per contenere cose che sono logicamente "oggetti" fatti di elementi in relazione gerarchica tra loro, ha bisogno di una GetChanges "Object Oriented" che estrae tutti gli ordini che hanno la testata o una delle righe modificate (che vuol dire aggiunte, editate o cancellate).

Questo è un requisito non solo per implementare correttamente logiche di business ma anche per implementare funzionalità logicamente proprie di un oggetto all'interno del proprio business layer : specificatamente con una GetChanges "object oriented" ho potuto facilmente implementare una politica di update basata su timestamp non della singola riga ma di tutto l'oggetto (se modifico una riga dell'ordine cambio il timestamp della testa ordine). Un altra cosa che ho potuto fare facilmente è salvare una copia di "backup/revisione/storia" (un po' alla source safe) di ogni oggetto che gestisco ogni volta che un elemento a qualunque livello della gerarcia dell'oggetto è modificato.

Qui di seguito trovate il codice per implementare la mia GetChanges, chiaramente funziona non solo con un solo livello di gerarchia ma con una struttura di tabelle gerachicamente "innestate" a piacimento.

Il codice non è il massimo dell'eleganza anche per la necessità di scrivere codice "particolare" per gestire (navigare, estrarre) le righe in stato deleted.

 

        public DataSet GetFullChanges(DataSet p_Originalds,string p_rootTable) {
            DataSet _Smartds = (DataSet)Activator.CreateInstance(p_Originalds.GetType());
            // cosi' mentre costruisco il Dataset non devo preoccuparmi
            // dell'integrità referenziale
            _Smartds.EnforceConstraints = false;

            DataTable _DataTable = p_Originalds.Tables[p_rootTable];
            ArrayList _ModifiedObjects = new ArrayList();
            // voglio trovare tutti i padri che
            // sono stati modificati o cancellati o hanno dei figli cancellati o modificati 
            foreach (DataRow _Row in _DataTable.Rows)
                if (pf_ThisOrChildrenHaveChanges(_Row))
                    _ModifiedObjects.Add(_Row);

            foreach (DataRow _Row in _ModifiedObjects) {
                pf_ImportRowAndChildren(_Smartds, _Row);
            }

            _Smartds.EnforceConstraints = true;
            return _Smartds;
        }

        private bool pf_ThisOrChildrenHaveChanges(DataRow p_Row) {
            // la riga è modificata esco
            if (p_Row.RowState != DataRowState.Unchanged) return true;

            foreach (DataRelation _Rel in p_Row.Table.ChildRelations) {
                // original mi da anche le cancellate ma non le nuove,
                // devo quindi chiederla due volte con parametro diverso
                // al primo giro la uso solo per trovare le figlie cancellate
                DataRow[] _ChildRows = p_Row.GetChildRows(_Rel, DataRowVersion.Original);
                foreach (DataRow _row in _ChildRows)
                    if (_row.RowState == DataRowState.Deleted) return true;

                // adesso la uso per esaminare ecursivamente le righe figlie (nuove o modificate)
                _ChildRows = p_Row.GetChildRows(_Rel, DataRowVersion.Current);
                foreach (DataRow _row in _ChildRows)
                    if (pf_ThisOrChildrenHaveChanges(_row)) return true;
            }
            // se arrivo qui la riga non è modificata, ne lo è uno dei suoi figli
            return false;
        }

        // importo dai figli a salire verso il padre
        // anche se non è strettamento necessario poichè sono con
        // enforceconstraints = false
        private void pf_ImportRowAndChildren(DataSet p_TargetSmartDS, DataRow p_SourceRow) {
            p_TargetSmartDS.Tables[p_SourceRow.Table.TableName].ImportRow(p_SourceRow);
            foreach (DataRelation _Rel in p_SourceRow.Table.ChildRelations) {
                DataRow[] _ChildRows = null;
                if (p_SourceRow.RowState == DataRowState.Deleted)
                    _ChildRows = p_SourceRow.GetChildRows(_Rel, DataRowVersion.Original);
                else
                    _ChildRows = p_SourceRow.GetChildRows(_Rel);

                foreach (DataRow _row in _ChildRows) {
                    // ricorsivamente importo la riga ed i suoi figli
                    pf_ImportRowAndChildren(p_TargetSmartDS, _row);
                }
            }
        }

11/19/2005 12:48:54 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Sto revisionando il 3° capitolo sul controllo del flusso, e ho pensato di mettere in forma di quiz un piccolo tip che spiego nel libro. Considerate il seguente codice:

For i As Integer = 1 To 10
  
Dim exiting As Boolean = False
  
For j As Integer = 1 To 20
     
' If the Evaluate function returns zero you want to exit both loops
     
If Evaluate(i, j) = 0 Then
        
exiting = True
        
Exit For
     
End If
   Next
   If exiting Then Exit For
Next

Non è importante sapere cosa fa la funzione Evaluate, ma solo che se questa funzione restituisce zero allora dovete uscire immediatamente da entrambi i cicli. Il ciclo di cui sopra non è molto ottimizzato, perchè deve testare in continuazione la variabile exiting. Si potrebbe ottimizzare il codice usando una istruzione Goto che punta a una etichetta che si trova dopo il secondo Next, ma sappiamo bene che le Goto sono per gli smanettoni, non per i programmatori seri come noi. Allora, la domanda del quiz è semplice:

E' possibile ottimizzare questo codice eliminando la variabile "exiting" ed uscendo dal ciclo più interno senza usare una istruzione Goto?

La risposta è semplice, quindi mi aspetto una soluzione in tempi stretti...

11/1/2005 10:10:33 AM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Un quiz di Adrian Florea mi ha ricordato una tecnica che uso talvolta per evitare chiamate ricorsive a un metodo. La tecnica "classica" per evitare la ricorsione consiste nel definire un campo booleano a livello di classe e testarlo all'entrata del metodo. Questa tecnica è spesso usata negli eventi di tipo TextChanged che modificano a loro volta la proprietà Text di un controllo, e che quindi farebbero scattare una ricorsione infinita:

Dim insideTextChanged As Boolean

Private Sub TextBox1_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles TextBox1.TextChanged
  
' Exit if this is a recursive call.
  
If insideTextChanged Then Exit Sub
  
' Forbid recursive calls from now on.
   insideTextChanged = True
  
' ...
  
TextBox1.Text = TextBox1.Text & " "
  
' Permit recursive calls.
  
insideTextChanged = False
End Sub

Questo approccio funziona, ma è decisamente scomodo perchè richiede un bel po' di codice. Se poi esiste anche la minima possibilità che possa avvenire una eccezione, allora occorre che tutto il corpo del metodo sia avvolto in un blocco Try, che imposta insideTextChanged a False nella sezione Finally. Perchè non usare allora una funzione che permette di testare se siamo all'interno di una chiamata ricorsiva? Penso ad esempio a qualcosa di questo tipo:

Private Sub TextBox1_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles TextBox1.TextChanged
  
' Exit if this is a recursive call.
  
If IsRecursive() Then Exit Sub
   ' ...
  
TextBox1.Text = TextBox1.Text & " "
End Sub

Ecco come implementare la funzione IsRecursive:

<System.Runtime.CompilerServices.MethodImpl(Runtime.CompilerServices.MethodImplOptions.NoInlining)> _
Public Shared Function IsRecursive() As Boolean
  
Dim st As New StackTrace
  
' Check whether any method in the call stack is the same as the immediate caller.
  
For n As Integer = 2 To st.FrameCount - 1
     
If st.GetFrame(1).GetMethod() Is st.GetFrame(n).GetMethod() Then Return True
  
Next
  
Return False
End Function

Ecco la versione C#:

[System.Runtime.CompilerServices.MethodImpl(Runtime.CompilerServices.MethodImplOptions.NoInlining)]
public static bool IsRecursive() 
{

  
StackTrace st = new StackTrace();
   
// Check whether any method in the call stack is the same as the immediate caller.
  
for ( int n= 2; n < st.FrameCount; n++ )
   {

     
if ( st.GetFrame(1).GetMethod() == st.GetFrame(n).GetMethod()
        
return true;
   
}
  
return false;
}

Il metodo controlla il metodo del chiamante immediato - ossia st.GetFrame(1).GetMethod() - con il metodo di tutti gli altri chiamanti sullo stack, e restitusce True se trova un match. E' essenziale che il metodo IsRecursive sia marcato con l'attributo MethodImpl in modo da evitare che il JIT-compiler esegua l'inline del metodo e faccia saltare il meccanismo di controllo. Nella versione attuale del JIT compiler questo problema non dovrebbe mai accadere, perchè il JIT compiler non esegue l'inlining dei metodi che contengono un loop, ma per il futuro non si puo' mai dire, e questo attributo ci mette al riparo da sorprese. Per lo stesso motivo, non si dovrebbe chiamare IsRecursive da un metodo che potrebbe essere ottimizzato mediante inlining.

Per chi non lo sapesse, l'inlining è una tecnica di ottimizzazione grazie alla quale il JIT-compiler riesce a spostare il codice di un metodo direttamente nel codice del metodo chiamante, in modo da evitare una istruzione di chiamata. Solo metodi molto piccoli, 32 byte di IL o meno, che non hanno cicli o loop, e che non accettano dei value type come argomenti possono essere ottimizzati in questo modo, nella versione attuale del JIT compiler, quindi nella pratica l'inlining avviene in pochi casi, tipicamente per le proprietà che wrappano una variabile e per i metodi con al massimo 2-3 righe di codice.

10/18/2005 12:10:17 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

J

 

Problema: docking di controlli .NET direttamente nella client area di Outlook con VSTO per Visual Studio 2005

 

Il file contenente il codice sorgente: HtmlViewModified.zip (256.37 KB)

 

Impegnato per conto di un cliente che sta ricrivendo una serie di applicazioni direttamente dentro Office System con .NET 2.0 e VS2005 – e Outlook 2003 in particolare - mi sono imbattuto in un “piccolissimo” problema non di poco conto. Benche’ con gli altri      prodotti di Office sia particolarmente semplice mostrare e usare form di ogni genere preparate in .NET, con Outlook 2003 ci siamo subito imbattuti in una serie di limiti che inizialmente sembravano insuperabili e decisamente antipatici. Per la precisione, a prima vista sembra che sia impossibile “dockare” le proprie form .NET all’interno dell’area client principale di lavoro di Outlook (l’area della inbox per intenderci) per scrivere applicazioni di produttivita’ utilizzando .NET, direttamente dentro Outlook. Pero’ ... con un po’ di fortuna e di intuito posso ora dire che l’empasse e’ durata relativamente poco. Infatti scaricato il preziosissimo esempio HtmlView disponibile sul sito di MSDN, che mostra come fare un po’ di report con del HTML plasmato via .NET, l’idea di ospitare le form (controlli windows form) .NET all’interno del HTML viewer (e’ il controllo del browser IE embedded dentro outlook) si e’ fatta subito strada. Devo ammettere che la mia bassissima conoscenza di HTML (che proprio non mi entra in testa - che ci volete fare) e’ stata, alla fine, la parte piu’ complessa del tutto. Per il resto il lavoro di inserimento delle form (controlli .NET) dentro Outlook e’ stato tutto in discesa e senza troppi problemi o intoppi. Descrivo brevemente per punti una serie di considerazioni e consigli da seguire per studiare il materiale allegato che mostra la soluzione del problema:

 

-          aggiornate la connection string nel .config che fa una piccola query sul db northwind per mostrare un diagramma a torte dentro un report embedded nella form schiacciando uno dei due pulsanti che troverete in testa alla vista dentro Outlook dopo aver lanciato l’applicazione e selezionata la cartella HtmlView figlia della cartella Inbox.

-          la classe BaseActiveXContainer (che ho copiato da qualche parte e modificato – l’autore mi perdoni per questo)  contiene del pratico codice per registrare i controlli .net come componenti COM

-          Generate tutte le volte un nuovo GUID per ogni nuovo controllo. Non usate la tecnica CCP (Cut Copy e Paste J - Copyright del mio amico Pierpaolo R.) con gli occhi bendati. Brutte sorprese vi aspettano.

-          Nel file AssemblyInfo.cs (aggiunto a manina perche’ in disuso con vs2005 – decideremo una best practice spero tra pochissimo ...) ho inserito degli attributi assembly wide per evitare problemi di security per caricare dll che non sopportano di essere chiamate da assembly “partially trusted”.Senza, molte LoadAssembly dinamiche rischiano di dare parecchi problemi.

 

[assembly: AllowPartiallyTrustedCallers]

[assembly: FileIOPermission(SecurityAction.RequestMinimum, Unrestricted = true)]

[assembly: SecurityPermission(SecurityAction.RequestMinimum, Execution = true)]

[assembly: RegistryPermission(SecurityAction.RequestMinimum, Unrestricted = true)]

[assembly: UIPermission(SecurityAction.RequestMinimum, Unrestricted = true)]

 

-          In testa alla classe HtmlViewContainer trovate una serie di attributi che vi permetteranno di riesporre attravers COM i vostri controlli .NET da “agganciare” dentro Office. La stessa classe contiene una serie di proprieta’ statiche che espongono globalmente nella applicazione i puntatori alle istanze di alcuni oggetti importanti di Outlook per interoperare con lo stesso con interfacce duali COM per supportare Automation.

-          Il file “.htm” del progetto allegato mostra come invocare metodi dell’oggetto ospitato dal HTML, che ospita il controllo .NET nella pagina, via jscript e automation.

-     Preparatevi mentalmente ad usare caspol o l'utility presente negli administrative tools per dare full trust ad eventuali DLL esterne bisognose di essere trattate in tal modo. Sappiate che il vostro assembly dentro la sandbox di Outlook e partially trusted. Vi consiglio di firmare digitalmente le vostre DLL e fare full trust su machine con le strong name degli assembly o direttamente un certificato buono. 

 

- altre ed eventuali ... prossimamente :) stay tuned

 

Una piccola schermata che mostra quello che si puo' fare dentro Outlook con .NET e VSTO per VS2005 completato in treno mentre tornavo a casa :). Nel codice trovate tutto quello che serve per far parlare il vostro controllo con Outlook dall'interno dell'explorer html di Outlook.

Enjoy !

Sto lavorando su dei proof of concept per Microsoft per Office Word ed Excel avanzatissimi. Spero di condividere al piu' presto con voi il materiale che sto producendo.

Giuseppe

10/7/2005 8:29:27 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Per prima cosa, un po' di background su cosa è la covariance e la contravariance dei delegate in C# 2.0. Supponiamo di avere un delegate di questo tipo:

   delegate object GetControlData(TextBox ctrl);

Per covariance si intende il fatto che è possibile fare puntare il delegate a un metodo il cui valore di ritorno eredita dal tipo di valore di ritorno specificato dal delegate. Ad esempio, poichè il delegate GetControlData restituisce un object, la covarianza permette di fare puntare il delegate a qualsiasi metodo, indipendentemente dal tipo di oggetto restituito, purchè il metodo accetti un oggetto TextBox in input. L'unica condizione è che il metodo restituisca effettivamente qualcosa, quindi non posso fare puntare il delegate a un metodo void. Ad esempio, potrei creare un oggetto GetControlData che punta a questo metodo, poichè il tipo string deriva da object:

   string GetText(TextBox ctrl) 
   { return ctrl.Text; }

Per contravariance si intende il fatto che è possibile fare puntare il delegate a un metodo il cui argomento è una classe base dell'argomento specificato nella signature del delegate. Nel caso dell'esempio, la controvarianza permette di fare puntare un delegate di tipo GetControlData a un metodo che accetta un oggetto di tipo Control oppure Object, che sono entrambi classi base per il tipo TextBox definito nel delegate:

   object GetTag(Control ctrl) 
   { return ctrl.Tag; }

E' evidente che la covarianza e controvarianza non creano problemi di robustezza nel codice e permettono di utilizzare un delegate per chiamare un metodo senza che si possano verificare errori di type mismatch a runtime. Ovviamente, covarianza e controvarianza possono essere combinate, ad esempio per fare puntare un delegate GetControlData al seguente metodo:

   string GetText(Control ctrl) 
   { return ctrl.Text; }

E' importante notare che - anche se il metodo target accetta un Control generico - richiamando il metodo tramite il delegate occorrerà passare un argomento di tipo TextBox, perchè questo è il tipo definito nella signature del delegate.

Fine della divagazione su queste feature: si tratta di informazioni che trovate praticamente dappertutto sulla rete, con molti esempi. Insieme ai tanti esempi, però, troverete ben poche applicazioni realmente interessanti per queste feature, ma questo è un altro discorso e ci tornerò a breve in un altro post. Per il momento voglio concentrarmi su un aspetto che riguarda più da vicino chi programma in Visual Basic, ovvero che Visual Basic 2005 non supporta nè la covarianza nè la controvarianza.

Tutto vero? si e no. Perchè è vero che VB 2005 non supporta queste feature direttamente, ma ci si può arrivare lo stesso. Infatti, a molti è sfuggito il fatto che il supporto per covarianza e controvarianza è stato aggiunto anche a livello del .NET Framework, tant'è vero che in .NET 2.0 è possibile usare reflection per creare dei delegate che godono di entrambe queste proprietà. Ecco allora come fare in Visual Basic 2005. Supponiamo di avere il seguente delegate e la seguente funzione all'interno di un Windows form:

   Delegate Function GetControlData(ByVal ctrl As TextBox) As Object

   Function GetText(ByVal ctrl As Control) As String
      Return ctrl.Text
   End Function

Poichè VB2005 non supporta direttamente covarianza e controvarianza non è possibile creare direttamente una istanza del delegate GetControlData che punta al metodo GetText. Però possiamo arrivarci comunque via reflection, creando un MethodInfo che punta al metodo target e passando poi questo MethodInfo al metodo statico Delegate.CreateDelegate.

' the target method
Dim targetMethod As MethodInfo = Me.GetType().GetMethod("GetText")
' build the delegate through reflection
Dim deleg As GetControlData = DirectCast([Delegate].CreateDelegate( _
    
GetType(GetControlData), Me, targetMethod), GetControlData)
' show that the delegate works correctly
Console.WriteLine(deleg(Me.TextBox1))

Questo codice è solo leggermente più lento della creazione diretta del delegate, ma non è un problema serio perchè tipicamente un delegate si crea una sola volta per poi usarlo ripetutamente. Un altro problema minore è che il codice potrebbe fallire a runtime se il nome del metodo target è scritto male, ma tutto sommato si tratta di un bug che si scoprirebbe al primo test del codice.

Da notare che, almeno con la CTP di Agosto (non ho ancora provato con la RC), il supporto di .NET alla covarianza e controvarianza è imperfetto, in quanto non tutti gli overload di Delegate.CreateDelegate supportano questa feature. Ad esempio, esiste un overload che accetta direttamente il nome del metodo (anzichè il MethodInfo che punta al metodo target), ma questo overload va in errore (ArgumentException: Error binding to target method) se si tenta di creare un delegate la cui signature non corrisponde esattamente al metodo target.


Come ho già accennato prima, la covarianza e controvarianza hanno alcuni usi pratichi molto interessanti, ed è un peccato che non siano state valorizzate come meritano. In particolare, la controvarianza permette di implementare tecniche ganzissime sia con Windows Forms che con Web Form, tecniche che in .NET 1.x richiedevano dei tripli salti mortali. Per darvi una idea, grazie alla controvarianza sono riuscito a risolvere in cinque minuti un problema che in .NET 1.1 mi prese a suo tempo per almeno quattro ore!

C'è qualcuno che ha qualche idea in proposito, prima di leggere il mio articolo che apparirà tra qualche giorno su questo sito?

10/4/2005 11:09:15 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Sto aggiustando il capitolo sul controllo del flusso di esecuzione, dove parlo (tra le variecose) anche delle funzioni ricorsive. Nella maggior parte dei libri di programmazione che conosco (incluso molti "classici") la ricorsione è trattata in modo superficiale con il tipico esempio del fattoriale (che può essere risolto in modo più efficiente con un ciclo For) oppure per visitare strutture ad albero. Sembrerebbe che la ricorsione non serva in una applicazione gestionale "normale", il che ovviamente non è vero. Come tutte le tecniche di programmazione, basta essere attenti a sfruttare le occasioni.

Ecco ad esempio un metodo che trasforma un numero nel suo corrispondente alfanumerico (ad es. 1234 in "One Thousand Two Hundreds Thirty Four"). Il codice è preso dal libro per Microsoft Press e quindi il risultato è in inglese, ma è facile ottenere una versione che emette il risultato in italiano:

Function NumberToText(ByVal n As Integer) As String
   Select Case n
      Case Is < 0
         Return "Minus " & NumberToText(-n)
      Case 0
         Return ""
      Case 1 To 19
         Dim arr() As String = {"One", "Two", "Three", "Four", "Five", "Six", _
            "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", _
            "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"}
         Return arr(n - 1) & " "
      Case 20 To 99
         Dim arr() As String = {"Twenty", "Thirty", "Forty", "Fifty", "Sixty", _
            "Seventy", "Eighty", "Ninety"}
         Return arr(n \ 10 - 2) & " " & NumberToText(n Mod 10)
      Case 100 To 199
         Return "One Hundred " & NumberToText(n Mod 100)
      Case 200 To 999
         Return NumberToText(n \ 100) & "Hundreds " & NumberToText(n Mod 100)
      Case 1000 To 1999
         Return "One Thousand " & NumberToText(n Mod 1000)
      Case 2000 To 999999
         Return NumberToText(n \ 1000) & "Thousands " & NumberToText(n Mod 1000)
      Case 1000000 To 1999999
         Return "One Million " & NumberToText(n Mod 1000000)
      Case 1000000 To 999999999
         Return NumberToText(n \ 1000000) & "Millions " & NumberToText(n Mod 1000000)
      Case 1000000000 To 1999999999
         Return "One Billion " & NumberToText(n Mod 1000000000)
      Case Else
         Return NumberToText(n \ 1000000000) & "Billions " _
            & NumberToText(n Mod 1000000000)
   End Select
End Function

Ecco la versione per gli amanti delle parentesi graffe. Poichè lo switch di C# non supporta i range, ho dovuto modificare il codice per usare una serie di istruzioni elseif. Un dettaglio interessante è come il codice costruisce un array di stringhe al volo per poi selezionare un elemento. E' possibile farlo anche in VB.NET, ma è una di quelle feature che piacciono soprattutto ai programmatori C#:

string NumberToText( int n)
{
   if ( n < 0 )
      return "Minus " + NumberToText(-n);
   else if ( n == 0 )
      return "";
   else if ( n <= 19 )
      return new string[] {"One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen"
         "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"}[n-1] + " ";
   else if ( n <= 99 )
      return new string[] {"Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"}[n / 10 - 2] + " " + NumberToText(n % 10);
   else if ( n <= 199 )
      return "One Hundred " + NumberToText(n % 100);
   else if ( n <= 999 )
      return NumberToText(n / 100) + "Hundreds " + NumberToText(n % 100);
   else if ( n <= 1999 )
      return "One Thousand " + NumberToText(n % 1000);
   else if ( n <= 999999 )
      return NumberToText(n / 1000) + "Thousands " + NumberToText(n % 1000);
   else if ( n <= 1999999 )
      return "One Million " + NumberToText(n % 1000000);
   else if ( n <= 999999999)
      return NumberToText(n / 1000000) + "Millions " + NumberToText(n % 1000000);
   else if ( n <= 1999999999 )
      return "One Billion " + NumberToText(n % 1000000000);
   else 
      return NumberToText(n / 1000000000) + "Billions " + NumberToText(n % 1000000000);
}

Non è codice complesso, ma non ho mai visto sui mille siti Internet una routine per la trasformazione di numeri in lettere altrettanto concisa ed efficace. Adoro la programmazione ad oggetti, i generics, gli attributi, e le regular expression, ma mi piace ricordare (e ricordarmi) che spesso si può scrivere ottimo codice, compatto e veloce, anche solo sfruttando feature che i principali linguaggi hanno da venti o trent'anni. :-)

9/27/2005 9:12:10 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Mentre sto rivedendo il capitolo sui generics, mi è tornato in mente un trucchetto semplice ma efficace per simulare i generics anche con .NET 1.x. Si tratta di sfruttare in modo poco ortodosso la capacità di definire degli alias per un tipo, mediante una direttiva Imports (VB) o using (C#). Ecco un esempio di quel che intendo, che implementa una collection di oggetti Widget (definiti da qualche parte nel namespace MyApplication):

' VB.NET
Imports ItemType = MyApplication.Widget

Public Class WidgetCollection
   Inherits CollectionBase

   Public Sub Add(ByVal item As ItemType)
      Me.List.Add(item)
   End Sub

   Public Sub Remove(ByVal item As ItemType)
      Me.List.Remove(item)
   End Sub

   Default Public Property Item(ByVal index As Integer) As ItemType
      Get
         Return CType(Me.List(index), ItemType)
      End Get
      Set(ByVal Value As ItemType)
         Me.List(index) = Value
      End Set
   End Property
End Class

// C#
using System;
using System.Collections;
using ItemType = MyApplication.Widget;

public class WidgetCollection: CollectionBase
{
   public void Add(ItemType item)
   {
      this.List.Add(item);
   }

   public void Remove(ItemType item)
   {
      this.List.Remove(item);
   }

   public ItemType this[int index]
   {
      get { return (ItemType) this.List[index]; }
      set { this.List[index] = value; }
   }
}

Quando avete bisogno di creare una collection di altro tipo, basta copiare il codice in un altro file, modificare il nome della classe e modificare l'istruzione Imports/using che definisce l'alias per ItemType. Ovviamente potete utilizzare questo meccanismo anche per simulare generics con due o più argomenti. Ad esempio, ecco una classe che implementa una hashtable contenente oggetti Widget e le cui chiavi sono stringhe:

' VB.NET
Imports ItemType = MyApplication.Widget
Imports KeyType = System.String

Public Class DictionaryTemplate
   Inherits DictionaryBase

   Public Sub Add(ByVal key As KeyType, ByVal item As ItemType)
      Me.Dictionary.Add(key, item)
   End Sub

   Public Sub Remove(ByVal key As KeyType)
      Me.Remove(key)
   End Sub

   Default Public Property Item(ByVal key As KeyType) As ItemType
      Get
         Return CType(Me.Dictionary(key), ItemType)
      End Get
      Set(ByVal Value As ItemType)
         Me.Dictionary(key) = Value
      End Set
   End Property
End Class

// C#
using System;
using System.Collections;
using ItemType = MyApplication.Widget;
using KeyType = System.String;

public class DictionaryTemplate : DictionaryBase
{
   public void Add(KeyType key, ItemType item)
   {
      this.Dictionary.Add(key, item);
   }

   public void Remove(KeyType key)
   {
      this.Dictionary.Remove(key);
   }

   public ItemType this[KeyType key]
   {
      get { return (ItemType) this.Dictionary[key]; }
      set { this.Dictionary[key] = value; }
   }
}

Questa tecnica aggiunge la possibilità di creare collection strong-typed in .NET 1.1 con la stessa facilità con cui si usano i generics in VS 2005, ma non implementa la seconda importante feature dei generics, ossia quella che permette di evitare il boxing dei tipi valore (come numeri e date). Per implementare anche questa feature occorre complicare notevolmente il codice: invece di ereditare semplicemente da CollectionBase occorre utilizzare un array tipizzato per contenere i valori ed implementare manualmente le interfacce ICollection, IList e IEnumerable. Ecco un esempio di collection di interi implementata in VB.NET .Il codice non è proprio banale, quindi lascio a voi la traduzione in C# (se proprio ci tenete):

Imports ItemType = System.Int32

Public Class ValueTypeCollectionTemplate
   Implements ICollection, IEnumerable, IList

   Dim items(15) As ItemType ' 16 items initially

   ' strong-typed members

   Public Sub Add(ByVal value As ItemType)
      If m_Count = items.Length Then ReDim Preserve items(items.Length * 2 - 1)
      items(m_Count) = value
      m_Count += 1
   End Sub

   Public Sub Insert(ByVal index As Integer, ByVal value As ItemType)
      If index < 0 OrElse index > m_Count Then Throw New ArgumentOutOfRangeException("Index was out of range.")
      If m_Count = items.Length Then ReDim Preserve items(items.Length * 2 - 1)
      Array.Copy(items, index, items, index + 1, m_Count - index)
      items(index) = value
      m_Count += 1
   End Sub

   Public Sub Remove(ByVal value As ItemType)
      Dim index As Integer = Me.IndexOf(value)
      If index >= 0 Then Me.RemoveAt(index)
   End Sub

   Default Public Property Item(ByVal index As Integer) As ItemType
      Get
         Return items(index)
      End Get
      Set(ByVal Value As ItemType)
         items(index) = Value
      End Set
   End Property

   Public Function IndexOf(ByVal value As ItemType) As Integer
      For i As Integer = 0 To m_Count - 1
         If items(i) = value Then Return i
      Next
      Return -1
   End Function

   Public Function Contains(ByVal value As ItemType) As Boolean
      Return IndexOf(value) <> -1
   End Function

   ' The IEnumerable interface

   Public Function GetEnumerator() As System.Collections.IEnumerator Implements IEnumerable.GetEnumerator
      ' Create an array that contains only the existing elements
      Dim res(m_Count - 1) As ItemType
      Array.Copy(items, res, m_Count)
      Return res.GetEnumerator()
   End Function

   ' The ICollection interface

   Private m_Count As Integer = 0

   Public ReadOnly Property Count() As Integer Implements ICollection.Count
      Get
         Return m_Count
      End Get
   End Property

   Public Sub CopyTo(ByVal array As Array, ByVal index As Integer) Implements ICollection.CopyTo
      array.Copy(items, 0, array, index, m_Count)
   End Sub

   Public ReadOnly Property IsSynchronized() As Boolean Implements ICollection.IsSynchronized
      Get
         Return False
      End Get
   End Property

   Public ReadOnly Property SyncRoot() As Object Implements ICollection.SyncRoot
      Get
         Return items
      End Get
   End Property

   Private Function ICollection_Add(ByVal value As Object) As Integer Implements IList.Add
      Me.Add(CType(value, ItemType))
   End Function

   Public Sub Clear() Implements IList.Clear
      ReDim items(15)
      m_Count = 0
   End Sub

   Private Function ICollection_Contains(ByVal value As Object) As Boolean Implements IList.Contains
      Return Me.Contains(CType(value, ItemType))
   End Function

   Private Function ICollection_IndexOf(ByVal value As Object) As Integer Implements IList.IndexOf
      Return Me.IndexOf(CType(value, ItemType))
   End Function

   Private Sub ICollection_Insert(ByVal index As Integer, ByVal value As Object) Implements IList.Insert
      Me.Insert(index, CType(value, ItemType))
   End Sub

   Public ReadOnly Property IsFixedSize() As Boolean Implements IList.IsFixedSize
      Get
         Return False
      End Get
   End Property

   Public ReadOnly Property IsReadOnly() As Boolean Implements IList.IsReadOnly
      Get
         Return False
      End Get
   End Property

   Private Property ICollection_Item(ByVal index As Integer) As Object Implements System.Collections.IList.Item
      Get
         Return Me.Item(index)
      End Get
      Set(ByVal Value As Object)
         Me.Item(index) = CType(Value, ItemType)
      End Set
   End Property

   Private Sub ICollection_Remove(ByVal value As Object) Implements IList.Remove
      Me.Remove(CType(value, ItemType))
   End Sub

   Public Sub RemoveAt(ByVal index As Integer) Implements IList.RemoveAt
      If index < 0 OrElse index > m_Count - 1 Then Throw New ArgumentOutOfRangeException("Index was out of range.")
      Array.Copy(items, index + 1, items, index, m_Count - index)
      m_Count -= 1
   End Sub
End Class

Se fate un po' di benchmark con grandi collection, vedrete che questa classe è decisamente più veloce di una collection class che deriva da CollectionBase.

Un'ultima nota, che vale sia per chi lavora con .NET 1.1 ma anche per chi è già passato armi e bagagli a .NET 2.0: questo trucchetto basato sugli alias in realtà ha alcuni vantaggi anche in VS 2005. Infatti, i generics non permettono di utilizzare il tipo argomento per specificare la classe base e inoltre non supportano le quattro operazioni aritmetiche se il tipo è numerico (come spiego in questo post). In altre parole, questo codice non compila:

// VB.NET 2005
Public Class MyGenericType(Of T)
   Inherits T ' <<<< errore

   Function Add(ByVal n1 As T, ByVal n2 As T) As T
      Return n1 + n2 ' <<<<< errore
   End Function
End Class

// C#
public class MyGenericType : T <<<<< errore
{
   public T Add(T n1, T n2)
   {
      return n1 + n2; // <<<<< errore
   }
}

E' evidente che entrambi i problemi si possono risolvere in modo "generico" usando la tecnica dell'alias. Uno sporco trucco, ma quando ce vo', ce vo' ! :-)

9/5/2005 7:04:44 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

O almeno questo è il titolo di questa pagina su MSDN :-) In realtà per ora io ne ho contate 47 di applicazioni di esempio, ma forse ne ne sono perse 3 perchè in fondo alla pagina c'è scritto "Coming soon - The remaining 51 samples are coming soon. Stay tuned." :-)

Ma non andiamo a pignolare, anche quello che c'è già è decisamente benvenuto! Le applicazioni sono fornite in C# o VB.NET e sono divise nelle seguenti categorie:

  • Base Class Libraries: come cambiare le ACL di un file, implementare animazioni su console, scaricare file da FTP, comprimere e decomprimere file, usare le generic collections, ricavare informazioni sui drive, usare la classe Stopwatch, eseguire il PING e qualche altra operazione di networking.
  • Data Access: query asincrone, uso e confronto di Datareader con DataSet, update batch, paginazione, bulk update, salvataggio e recupero di immagini dal DB, classi factory, uso di MARS, Notification Services, e UDT di SQL Server 2005.
  • Web Development: master pages, API e controlli di membership, profili, controlli menu e TreeView, web part, data binding con i vari componenti xxxDataSource.
  • Windows Forms: task asincroni, componenti BindingNavigator e BindingSource, estensioni alla DataGridView standard, i nuovi controlli MaskedTextBox, WebBrowser, StatusStrip, ToolStrip, SplitContainer e LayoutPanel.

Insomma, se ancora non avete dato un'occhiata a .NET 2.0 e VS.NET 2005, questi esempi pronti per essere eseguiti sono un'ottima occasione per vedere velocemente alcune delle novità introdotte. Complimenti per l'iniziativa, e come dice la pagina stessa: tenete d'occhio la pagina per gli ulteriori 51 (o 54?) esempi che hanno già promesso.

8/8/2005 1:12:48 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

In questi giorni sto lavorando al capitolo sulla serializzazione con .NET 2.0 e mi sono subito imbattuto in un problema imprevisto. La nuova versione del framework supporta un nuovo attributo chiamato OptionalField, che serve a marcare i campi aggiunti ad una nuova versione di una classe. Ecco in pratica come questo attributo dovrebbe funzionare (il condizionale è d'obbligo, come vedrete). Supponiamo di avere una classe Person nella versione 1.0.0.0 di un assembly BOLibrary (marcato con uno strong-name).

Class Person
   Public Name As String
   ' altri campi omessi...
End Class

Supponiamo ora di aggiornare il nostro assembly alla versione 1.1.0.0 e di aggiungere un nuovo campo ID. Se però tentiamo di deserializzare con la nuova versione uno stream serializzato con la prima versione avviene un SerializationException. Questo errore può essere aggirato proprio grazie al nuovo attributo:

Class Person
   Public Name As String
   <OptionalField()> Public ID As String
   ' altri campi omessi...
End Class

Il primo problema da risolvere, però, è che in realtà il .NET runtime tenterà comunque di deserializzare la versione 1.0.0.0 della classe, quindi l'unico modo per evitare un'altra SerializationException è di impostare la proprietà AssemblyFormat dell'oggetto BinaryFormatter al valore Simple, in questo modo:

Dim p As New Person()
Dim bf As New BinaryFormatter()
' non inserire il nome completo dell'assembly nello stream
bf.AssemblyFormat = FormatterAssemblyStyle.Simple
Dim fs As New FileStream("c:\person.dat", FileMode.Create)
bf.Serialize(fs, p)

Questo è almeno il codice che si trova in giro su Internet, in numerosi blog e articoli online. Peccato che con la Beta 2 questo codice non funziona più :-( Il motivo è semplice: in questa beta la proprietà AssemblyFormat viene ignorata per il BinaryFormatter, e viene onorata solo dal SoapFormatter (che però nel frattempo è stato dichiarato obsoleto e non si dovrebbe più usare). Quindi l'attributo OptionalField funziona come descritto nei vari esempi solo con gli assembly non segnati con uno strong-name, in cui il problema del versioning non esiste neanche. La cosa strana è che questo "dettaglio" non è documentato da nessuna parte e solo grazie a qualche "amicizia altolocata" a Redmond ho avuto la conferma che questo comportamento è by-design, non è un bug della Beta 2, e quindi lo ritroveremo pari pari nella versione definitiva (se non arrivano cambi di rotta nel frattempo).

Poichè è buona norma marcare con uno strong-name tutti i propri assembly - sia EXE che DLL - sia per applicare un numero di versione che per attivare la CAS che per lavorare con ClickOnce, è necessario quindi trovare un metodo per usare assembly con strong-name e allo stesso tempo poter deserializzare una istanza serializzata con una versione precedente. Altrimenti tutto il meccanismo della version tolerant deserialization (VTS) va a farsi benedire. Per fortuna la soluzione è a portata di mano, sotto forma della proprietà Binder.

La prima cosa da fare per poter usare questa proprietà è definire un tipo che deriva dalla classe astratta SerializationBinder. Questa classe ha un metodo astratto BindToType, che viene chiamato quando il BinaryFormatter deve deserializzare un tipo. Ecco una classe di questo tipo, che reindirizza tutti i tipi presenti nella versione 1.0.*.* della libreria BOLibrary alla versione 1.1.0.0 della stessa libreria:

Class MySerializationBinder
   Inherits SerializationBinder

   Public Overrides Function BindToType(ByVal assemblyName As String, ByVal typeName As String) As Type
      ' Read the version of the assembly.
      Dim an As New AssemblyName(assemblyName)
      If an.Name = "BOLibrary" AndAlso an.Version.Major = 1 AndAlso an.Version.Minor = 0 Then
         ' Replace the version number with the current version number.
         an.Version = New Version("1.1.0.0")
         ' Load the new assembly.
         Dim asm As Assembly = Assembly.Load(an.FullName)
         ' Return the type taken from that assembly
         Return asm.GetType(typeName)
      Else
        ' Otherwise, tell the runtime to apply the default binding policy.
        Return Nothing
      End If
   End Function
End Class

Ecco come si usa questa nuova classe:

Dim bf As New BinaryFormatter()
bf.Binder = New MySerializationBinder()

Gli oggetti SerializationBinder risolvono il problema in questione, ma in realtà sono molto più potenti di tanto. Ad esempio, potete usarli se avete spostato una classe in un assembly differente, oppure potete dire al .NET runtime di deserializzare lo stream in una classe con nome differente (ma con dei campi con lo stesso nome e tipo della classe originaria), ecc. E' un meccanismo molto potente che offre una enorme flessibilità durante la fase di deserializzazione. E senza il quale non potrete mai sfruttare il nuovo attributo OptionalField.

8/4/2005 11:02:32 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Ho finalmente finito il capitolo sui generics, che ha richiesto più tempo del previsto a causa di tanti impegni che si sono succeduti in questi giorni. Ci sono grandi novità in Code Architects e non sempre resta abbastanza tempo per scrivere. Comunque sono molto soddisfatto del risultato, perchè in queste 40 pagine circa sono riuscito a condensare non solo tutte le proprietà documentate dei generics ma anche e soprattutto un bel po' di tecniche di programmazioni meno banali. In questo post mostro ad esempio come risolvere un problema abbastanza comune con i generics. (Per la gioia di coloro che sono allergici alle parole chiave Me e Of, ho tradotto tutti gli esempi in C#.)

 

Una delle limitazioni dei generics in cui ci si imbatte quasi subito è il fatto che un generic non può usare alcun operator sugli elementi del tipo passato come argomento. Mi spiego meglio. Supponiamo di voler creare una classe che deriva da List<T> e che aggiunge un metodo che restituisce la somma degli elementi: 

' VB
Public Class StatsList(Of T)
   Inherits List(Of T)

   Public Function Sum() As T
      Dim result As T = Nothing
      For Each item As T In Me
         result += item ' *** Compile error
      Next
      Return result
   End Function
End Class

// C#
class StatsList<T> : List<T>
{
   public T Sum()
   {
      T result = default(T);
      foreach (T item in this)
         result += item; // *** Compile error
      return result;
   }
}

I commenti indicano quali istruzioni causano un errore di compilazione, che è ovviamente causato dal fatto che il compilatore non può essere sicuro che il tipo T supporti l'operatore +. Questo è un serio limite nella applicazione dei generics, come mi sono reso conto quasi immediatamente quando ho cominciato a sviluppare con Visual Studio 2005. Ricordo di aver letto alcuni blog sulla rete che lamentavano lo stesso problema, e non mi pare di aver trovato una soluzione davvero soddisfacente.

 

In realtà, a mio avviso si tratta di una svista a cui Microsoft avrebbe potuto rimediare abbastanza facilmente quando .NET 2.0 era in alpha. Dato che siamo nelle fasi finali della Beta 2 dubito fortemente che faranno qualcosa in proposito prima del rilascio ufficiale. Per rimediare a questo problema, infatti, sarebbe bastato che Microsoft avesse definito una interfaccia contenente i metodi per le quattro operazioni, ad esempio:

 

' VB
Public Interface IOperators(Of T)
   Function Add(ByVal n1 As T) As T
   Function Subtract(ByVal n As T) As T
   Function Multiply(ByVal n As T) As T
   Function Divide(ByVal n As T) As T
End Interface

// C#
public interface IOperators<T>
{
   T Add(T n);
   T Subtract(T n);
   T Multiply(T n);
   T Divide(T n);
}

Se questa interfaccia fosse esposta da tutti i tipi numerici base del .NET Framework, sarebbe molto semplice aggiungere un constraint alla classe StatsList, in modo che il codice nella classe possa eseguire le operazioni aritmetiche sul tipo T per mezzo della interfaccia:

' VB
Public Class StatsList(Of T As IOperators(Of T))
   Inherits List(Of T)

   Public Function Sum() As T
      Dim result As T = Nothing
      For Each item As T In Me
         result = result.Add(item)
      Next
      Return result
   End Function
End Class

// C#
class StatsList<T> : List<T> where T:IOperators<T>
{
   public T Sum()
   {
      T result = default(T);
      foreach (T item in this)
         result = result.Add(item);
      return result;
   }
}

Questo concetto non è completamente nuovo. Ad esempio, tutti i tipi numerici (e altri tipi primitivi, come String) supportano l'interfaccia IComparable<T>, che infatti permette di supplire alla impossibilità di usare gli operatori di confronto nelle classi generic. Avrebbero potuto aggiungere allo stesso modo una interfaccia come IOperators, ma la realtà è che in Microsoft non ci hanno pensato e adesso, per bene che ci vada, dovremo aspettare la prossima versione del .NET Framework.

 

Tuttavia, anche se non possiamo modificare l'implementazione dei tipi primitivi come Int32 o Double, possiamo però creare una classe ausiliaria che esegua per noi queste operazioni. Questo concetto è molto simile a quello che accade con le interfacce IComparable<T> e IComparer<T>. Se una classe implementa IComparable<T> essa è in grado di confrontare i suoi elementi in modo autonomo; se però non è questo il caso, possiamo comunque definire una classe ausiliaria che implementa IComparer<T> e che dichiara quindi di essere in grado di confrontare due valori di tipo T.

 

Se spostiamo il concetto sulle quattro operazioni aritmetiche, possiamo quindi definire una interfaccia ICalculator<T>; una classe può implementare tale interfaccia per dichiarare che è in grado di eseguire queste operazioni su elementi di tipo T: 

' VB
Public Interface ICalculator(Of T)
   Function Add(ByVal n1 As T, ByVal n2 As T) As T
   Function Subtract(ByVal n1 As T, ByVal n2 As T) As T
   Function Multiply(ByVal n1 As T, ByVal n2 As T) As T
   Function Divide(ByVal n1 As T, ByVal n2 As T) As T
   Function ConvertTo(ByVal n As Object) As T
End Interface

// C#
public interface ICalculator<T>
{
   T Add(T n1, T n2);
   T Subtract(T n1, T n2);
   T Multiply(T n1, T n2);
   T Divide(T n1, T n2);
   T ConvertTo(object n);
}

Non resta ora che implementare una classe che implementi ICalculator<Int32>, un'altra classe che implementa ICalculator<Double>, e così via. Per ridurre il numero delle classi con cui avremo a che fare, possiamo fare anche di meglio: possiamo definire una unica classe che implementa l'interfaccia ICalculator<T> per tutte le classi numeriche che ci interessano. È un bel po' di codice, ma la sua struttura è davvero semplice:  

' VB
Public Class NumericCalculator
   Implements ICalculator(Of Int32)
   Implements ICalculator(Of Double)

   ' The ICalculator(Of Int32) interface
   Public Function AddInt32(ByVal n1 As Int32, ByVal n2 As Int32) As Int32 _
         Implements ICalculator(Of Int32).Add
      Return n1 + n2
   End Function
   Public Function SubtractInt32(ByVal n1 As Int32, ByVal n2 As Int32) As Int32 _
         Implements ICalculator(Of Int32).Subtract
      Return n1 - n2
   End Function
   Public Function MultiplyInt32(ByVal n1 As Int32, ByVal n2 As Int32) As Int32 _
         Implements ICalculator(Of Int32).Multiply
      Return n1 * n2
   End Function
   Public Function DivideInt32(ByVal n1 As Int32, ByVal n2 As Int32) As Int32 _
         Implements ICalculator(Of Int32).Divide
      Return n1 \ n2
   End Function
   Public Function ConvertToInt32(ByVal n As Object) As Int32 _
         Implements ICalculator(Of Int32).ConvertTo
      Return CInt(n)
   End Function

   ' The ICalculator(Of Double) interface
   Public Function AddDouble(ByVal n1 As Double, ByVal n2 As Double) As Double _
         Implements ICalculator(Of Double).Add
      Return n1 + n2
   End Function
   Public Function SubtractDouble(ByVal n1 As Double, ByVal n2 As Double) As Double _
         Implements ICalculator(Of Double).Subtract
      Return n1 - n2
   End Function
   Public Function MultiplyDouble(ByVal n1 As Double, ByVal n2 As Double) As Double _
         Implements ICalculator(Of Double).Multiply
      Return n1 * n2
   End Function
   Public Function DivideDouble(ByVal n1 As Double, ByVal n2 As Double) As Double _
         Implements ICalculator(Of Double).Divide
      Return n1 / n2
   End Function
   Public Function ConvertToDouble(ByVal n As Object) As Double _
         Implements ICalculator(Of Double).ConvertTo
      Return CDbl(n)
   End Function
End Class

// C#
public class NumericCalculator 
   : ICalculator<Int32>, ICalculator<Double>
{
   // The ICalculator<Int32> interface
   Int32 ICalculator<Int32>.Add(Int32 n1, Int32 n2)
   {
      return n1 + n2;
   }
   Int32 ICalculator<Int32>.Subtract(Int32 n1, Int32 n2)
   {
      return n1 - n2;
   }
   Int32 ICalculator<Int32>.Multiply(Int32 n1, Int32 n2)
   {
      return n1 * n2;
   }
   Int32 ICalculator<Int32>.Divide(Int32 n1, Int32 n2)
   {
      return n1 / n2;
   }
   Int32 ICalculator<Int32>.ConvertTo(object n)
   {
      return Convert.ToInt32(n);
   }

   // The ICalculator<Double> interface
   Double ICalculator<Double>.Add(Double n1, Double n2)
   {
      return n1 + n2;
   }
   Double ICalculator<Double>.Subtract(Double n1, Double n2)
   {
      return n1 - n2;
   }
   Double ICalculator<Double>.Multiply(Double n1, Double n2)
   {
      return n1 * n2;
   }
   Double ICalculator<Double>.Divide(Double n1, Double n2)
   {
      return n1 / n2;
   }
   Double ICalculator<Double>.ConvertTo(object n)
   {
      return Convert.ToDouble(n);
   }
}

Ora finalmente possiamo riscrivere la classe StatsList correttamente; in questa nuova versione la classe accetta due argomenti generici: il tipo T degli elementi della list e il tipo C che è in grado di eseguire operazioni aritmetiche sul tipo T. Un accorgimento importante è nel definire correttamente i constraint del tipo C, che deve essere instanziabile e deve implementare l'interfaccia generica ICalculator<T>: 

' VB
Public Class StatsList(Of T, C As {New, ICalculator(Of T)})
   Inherits List(Of T)

   ' The object used as a calculator
   Dim calc As New C

   Public Function Sum() As T
      Dim result As T
      For Each elem As T In Me
         result = calc.Add(result, elem)
      Next
      Return result
   End Function

   Public Function Avg() As T
      Return calc.Divide(Me.Sum, calc.ConvertTo(Me.Count))
   End Function
End Class

// C#
class StatsList<T, C> : List<T> where C : ICalculator<T>, new()
{
   // an instance of the number calculator
   private C calc = new C();

   // return the sum of all elements in the list
   public T Sum()
   {
      T result = default(T);
      foreach (T item in this)
         result = calc.Add(result, item);
      return result;
   }

   // return the average of all elements in the list
   public T Avg()
   {
      return calc.Divide(Sum(), calc.ConvertTo(this.Count));
   }
}

Ecco come si usa la nuova versione della classe StatsList: 

' VB
Dim list As New StatsList(Of Double, NumericCalculator)
' fill the list with some values
For i As Integer = 1 To 10
   list.Add(i)
Next
Console.WriteLine("Sum = {0}", list.Sum())
Console.WriteLine("Average = {0}", list.Avg())

// C#
StatsList<Double, NumericCalculator> list;
list = new StatsList<double, NumericCalculator>();
// fill the list with some values
for ( int i = 1; i <= 10; i++ )
   list.Add(i);
Console.WriteLine("Sum = {0}", list.Sum());
Console.WriteLine("Average = {0}", list.Avg());

Per una soluzione davvero completa occorre estendere la classe NumericCalculator per supportare tutti i tipi numerici di .NET. Per farlo occorre fare un po' di copia-e-incolla, oppure potete utilizzare le versioni VB e C# che ho preparato per voi e che potete scaricare qui.

 

7/21/2005 1:19:19 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Mentre aspetto la risposta al sesto quizzettino :) (lo trovate come decimo commento al post precedente) ..

Lo sapevate che l'infrastruttura del ResourceManager è estendibile ? , cioè che potete far si che le risorse non siano caricate dagli assembly satellity ma da un qualunque altro storage di vostro piacimento ? .. behh io lo so da poco :) :  tutto è cominciato quando ho approfondito il significato del costruttore più complesso della classe ResourceManager il quale accetta come ultimo parametro un Type di nome usingResourceSet.

La documentazione fa capire vagamente che quella è la via per estendere l'infrastruttura di .NET preposta al reperimento delle risorse a runtime. Per fortuna ho trovato questo post http://weblogs.asp.net/cnagel/archive/2003/07/06/9751.aspx in cui è spiegato come fare (ed è fornito anche del codice di esempio). L'implementazione che si può scaricare dal post che vi ho linkato prevede di andare a reperire le risorse da un database.
Nell'implementazione mia la classe che derivo da ResourceManager vi fa scegliere se andare per la strada standard o usare uno storage alternativo che è un file XML (lo si decide in base al costruttore usato).

La cosa "carina" è che mi metto in ascolto delle modifiche al file XML e quando questo è modificato invalido la cache del ResurceManager così che alla prossima richiesta le risorse sono caricate fresche (chiamate la Dispose quando "avete finito" o il ResourceManage non va giù perchè un suo metodo si è sottoscritto alla notifica della modifica del file).

SmartResources.zip (4.76 KB)
7/20/2005 3:26:47 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Con mia sorpresa - e un po' di delusione - devo constatare che questa volta nessuno ha risposto il quizzettino che avevo proposto sui generics, se si eccettua il "solito" Stefano. Mi piacerebbe leggere qualche vostro commento, per capire se il quizzettino era troppo difficile, oppure era su un argomento che ancora in pochi conoscono, o semplicemente se siete già tutti al mare :-)

Comunque, nella sua semplicità il quiz era abbastanza interessante. Se con l'object browser si dà una occhiata ai metodi dell'oggetto List<T> si nota un metodo generico che potrebbe fare al caso nostro. Il metodo FindAll, infatti, restituisce un altro oggetto List<T> che contiene solo gli elementi della lista che soddisfano l'azione indicata da un delegate di tipo Predicate<T>. Allora potrei usare il metodo FindAll sull'oggetto List2 e nel predicato testo la condizione che l'elemento sia contenuto anche in List1, ossia qualcosa del genere:

Dim list1 As New List(Of Double)(New Double() {1, 2, 3, 4, 5, 6, 7, 8, 9})
Dim list2 As New List(Of Double)(New Double() {0, 3, 6, 9, 12})
Dim list3 As List(Of Double)

Sub SolveQuiz
   ' Find all eleemnts of list2 that appear also in list2, and remove them from list1
   Dim list3 As List(Of Double) = list2.FindAll(AddressOf FindInList1)
   ' Remove all the elements in list1/list2 that appear also in list3
   list1.RemoveAll(AddressOf FindInList3)
   list2.RemoveAll(AddressOf FindInList3)
End Sub

Function FindInList1(ByVal n As Double) As Boolean
   Return list1.Contains(n)
End Function

Function FindInList3(ByVal n As Double) As Boolean
   Return list3.Contains(n)
End Function

Questa soluzione è poco elegante, anche perchè costringe a dichiarare le tre liste a livello di classe, per metterle a disposizione delle tre procedure. In C# si può aggirare il problema usando gli anonymous methods:

List<double> list1 = new List<double>(new double[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
List<double> list2 = new List<double>(new double[] { 0, 3, 6, 9, 12});

List<double> list3 = list2.FindAll( delegate(double n)
                                  { return list1.Contains(n);});
list1.RemoveAll(delegate(double n)
                                  { return list3.Contains(n); });
list2.RemoveAll(delegate(double n)
                                  { return list3.Contains(n); });

Se contiamo le istruzioni negli anonymous methods (ma non le dichiarazioni di list1 e list2), anche la soluzione C# non riesce a scendere sotto le 6 istruzioni. Quindi probabilmente vi stupirete se vi dico che si può fare con soltanto DUE istruzioni?

La tecnica è molto semplice, una volta che si comprende il trucco. Tutti i libri e gli articoli che ho letto mostrano come usare i metodi generici di Array, List, Dictionary e altri classe con dei delegate che puntano a dei metodi definiti nella propria applicazione o, nel caso di C#, che puntano a degli anonymous methods. Ma i delegate possono puntare a metodi definiti nel framework, purchè ovviamente tali metodi rispettino la signature del delegate. In altre parole, è possibile scrivere del codice molto più compatto come segue:

' VB
Dim list3 As List(Of Double) = list2.FindAll(AddressOf list1.Contains)
list1.RemoveAll(AddressOf list3.Contains)
list2.RemoveAll(AddressOf list3.Contains)

// C#
List<double> list3 = list2.FindAll(new Predicate<double>(list1.Contains));
list1.RemoveAll(new Predicate<double>(list3.Contains));
list2.RemoveAll(new Predicate<double>(list3.Contains));

E così siamo riusciti a ridurre le istruzioni da 6 a 3. Come fare per eliminarne ancora una? Basta ricordare che il metodo Remove di List<T> restituisce True se l'elemento specificato era effettivamente presente nella lista, quindi possiamo passarlo direttamente come predicato per il metodo FindAll:

' VB
Dim list3 As List(Of Double) = list2.FindAll(AddressOf list1.Remove)
list2.RemoveAll(AddressOf list3.Contains)

// C#
List<double> list3 = list2.FindAll(list1.Remove);
list2.RemoveAll(list3.Contains);

... dove in C# ho approfittato della nuova feature del delegate inference (che il VB.NET ha da qualche anno, :-) ).

Certo non si può dire che questo codice sia il massimo della leggibilità, ma di certo è un buon esempio di quello che si riesce a fare con i generics. Che ne dite?


Ora che vi ho spiegato il meccanismo, vi lascio con un piccolo quizzettino accessorio, la cui soluzione si basa sulla medesima tecnica:

Data un oggetto List1 di tipo List<int> che contiene un numero qualsiasi di interi (es 10,16,32), ottenere una nuova lista List2<string> che contiene la rapresentazione esadecimale dei numeri in List1 (es. "A", "10", "20") con una sola istruzione e senza usare anonymous methods.


 

6/30/2005 1:10:23 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Il sistema più semplice per eseguire una shallow copy di un oggetto (ossia, creare una copia dell'oggetto ma non dei suoi oggetti "figli") è chiamare il metodo MemberwiseClone, che tutti i tipi .NET ereditano da System.Object. Ecco ad esempio come implementare il metodo Clone della interfaccia IClonable in una propria classe:

' VB.NET
Class TestClass
   Implements ICloneable

   Public Function Clone() As Object Implements ICloneable.Clone
      Return Me.MemberwiseClone()
   End Function
End Class

// C#
class TestClass : ICloneable
{
   public object Clone()
   {
      return this.MemberwiseClone();
   }
}

Come fare quando vogliamo clonare una istanza di un tipo che NON abbiamo definito nel programma? Proprio oggi ho dovuto risolvere questo problema con un Data Adapter. La soluzione è davvero semplice, almeno se l'applicazione è full trusted o almeno ha le permission per usare reflection: è sufficiente richiamare MemberwiseClone via reflection, no? Ecco un metodo riutilizzabile che esegue la copia di qualsiasi oggetto:

' VB.NET
Public Function CloneObject(ByVal obj As Object) As Object
   Return obj.GetType().InvokeMember("MemberwiseClone", BindingFlags.NonPublic Or BindingFlags.InvokeMethod Or BindingFlags.Instance, Nothing, obj, Nothing)
End Function

// C#
public object CloneObject(object obj)
{
   return obj.GetType().InvokeMember("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.InvokeMethod | BindingFlags.Instance, null, obj, null);
}

Attenzione a un dettaglio: quando si usa MemberwiseClone per creare un nuovo oggetto, il costruttore dell'oggetto non viene eseguito. Se il costruttore contiene del codice che esegue una operazione che non sia la semplice inizializzazione di un field - ad es. incrementa un contatore globale o apre un file - occorre adottare una tecnica differente.

6/17/2005 1:05:04 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Un Web Service che ha scelto il DataSet come formato di scambio dati è un pessimo esempio di interoperabilità. Esso in realtà non è utlizzabile da client non .NET (se non in forma de-tipizzata lavorando a livello di XML).

Come fare però se la nostra applicazione usa già internamente i DataSet e si vogliono pubblicare alcuni dei suoi servizi come Web Services ? Occorre travasare in qualche modo i dati avanti e indietro nei due formati: i dataset sono convertiti in oggetti tipizzati che sono restituiti dal Web Service; parimenti, un oggetto tipizzato ricevuto da un Web Service, viene convertito in un Dataset prima di essere passato ai livelli sottostanti dell'applicazione.

Fortunatamente la cosà puo' essere automatizzata e generalizzata sfruttando le similutidini tra la rappresentazione XML dei dataset, così come è restituita dai metodi ReadXml/WriteXml ed il formato con cui il serializzatore XML rappresenta proprietà e metodi pubblici delle classi.

Occorre prima di ciò però affrontare ancora un altro problema: come generare tali classi tipizzate ? Escludendo l'ipotesi di farlo manualmente esistono due soluzioni : a) usare il tool XSD b) creare un proprio generatore di codice che genera il codice delle classi tipizzate basandosi sullo schema del Dataset. Per avere il pieno controllo sul codice generato ho percorso la seconda alternativa ottenendo la seguente classe (ai volenterosi l'uso di CodeDom per fare la stessa cosa :) ) :

using System;
using System.Text ;
using System.Data ;

namespace ds2ws {
 public class GenerateTypedClass    {
  private GenerateTypedClass() {}

  public static string Go(DataSet p_ds,string p_rootnamespace) {
   StringBuilder l_sb = new StringBuilder ();
   pf_emitDSEnterBlock(p_ds,l_sb,p_rootnamespace);
   foreach(DataTable l_dt in p_ds.Tables) {
      if(l_dt.ParentRelations.Count >1) throw new Exception ("Table " + l_dt.TableName 
         + " has more than one incoming relation");                 
      if(l_dt.ParentRelations.Count ==0) 
         pf_EmitArrayContainer(l_dt,l_sb); 
   } 

   foreach(DataTable l_dt in p_ds.Tables) {
      if(l_dt.ParentRelations.Count >1) throw new Exception ("Table " + l_dt.TableName + " has more than one incoming relation"); 
      if(l_dt.ParentRelations.Count ==0) pf_EmitTable(l_dt,l_sb);
   } 
   l_sb.Append (c_ExitBlock);             
   l_sb.Append (c_ExitBlock);             
   return l_sb.ToString ();
 }

 private static void pf_EmitArrayContainer(DataTable p_dt, StringBuilder p_sb) {
  p_sb.Append (c_MemberList.Replace("%dsname%",p_dt.DataSet.DataSetName).Replace("%tablename%",p_dt.TableName.Replace(' ','_')));
 }

 private static void pf_EmitTable(DataTable p_dt, StringBuilder p_sb) {
   p_sb.Append (c_TableEnterBlock.Replace("%dsname%",p_dt.DataSet.DataSetName).Replace("%tablename%",p_dt.TableName.
      Replace(' ','_')).Replace("%namespace%",p_dt.DataSet.Namespace)); 
   foreach(DataColumn l_column in p_dt.Columns) 
      p_sb.Append (c_Property.Replace("%columnname%",l_column.ColumnName.Replace (' ','_'))
       .Replace("%datatype%",l_column.DataType.Name)); 

   foreach(DataRelation l_rel in p_dt.ChildRelations) 
      pf_EmitArrayContainer(l_rel.ChildTable ,p_sb);
   p_sb.Append (c_ExitBlock); 
   foreach(DataRelation l_rel in p_dt.ChildRelations) 
      pf_EmitTable(l_rel.ChildTable ,p_sb);


static string c_DSEnterBlock = @"
using System;
namespace %rootnamespace%.WS {
using System.Xml.Serialization;
[System.Xml.Serialization.XmlTypeAttribute(Namespace=""%namespace%"")]
[System.Xml.Serialization.XmlRootAttribute(Namespace=""%namespace%"", IsNullable=false)]
public class %dsname% {"
 
+ Environment.NewLine
+ @" private string m_version; public string Version { get { return m_version;} set { m_version=value; }}" + Environment.NewLine 
+ Environment.NewLine ;

static
string c_TableEnterBlock = @"[System.Xml.Serialization.XmlTypeAttribute(Namespace=""%namespace%"")]
   public class %dsname%%tablename% {"
+ Environment.NewLine ;

static string c_ExitBlock = @"}" + Environment.NewLine ;

static string c_Property = @"private %datatype% m_%columnname%; public %datatype% %columnname% { get { return m_%columnname%;} set 
   { m_%columnname%=value; }}"
+ Environment.NewLine ;

static string c_MemberList =@"[System.Xml.Serialization.XmlElementAttribute(""%tablename%"", typeof(%dsname%%tablename%))] 
   public %dsname%%tablename%[] %dsname%%tablename%Items;"
+ Environment.NewLine ;

private static void pf_emitDSEnterBlock(DataSet p_ds, StringBuilder p_sb,string p_rootnamespace) {
   p_sb.Append (c_DSEnterBlock.Replace("%dsname%",p_ds.DataSetName).Replace("%namespace%",p_ds.Namespace).Replace("%
      rootnamespace%"
,p_rootnamespace));             
}
}
}

 

Il metodo di entrata ha nome Go. Riceve un Dataset ed una stringa che rappresenta il namespace all'interno del quale deve essere creata la classe tipizzata. Del dataset è evidentemente rilevante la sua struttura e non i dati eventualemnte contenuti.

Salvato come file la stringa restituita dal metodo GO ed incluso tale file nel progetto, andiamo a vedere come travasare i dati da un dataset alla sua classe tipizzata corrispondente e viceversa:

demo _demo = new demo();
//Fill the Dataset
System.Xml.Serialization.XmlSerializer _XmlSerializer
= new System.Xml.Serialization.XmlSerializer(typeof(mynamespace.WS.demo));
foreach(DataRelation _rel in _demo.Relations)
_rel.Nested =true;
System.IO.MemoryStream _MemoryStream = new System.IO.MemoryStream ();
_demo.WriteXml(_MemoryStream);
_MemoryStream.Position=0;
// move data to the typed class generated by the code generator shown above
mynamespace.WS.demo _TypedDemo= (mynamespace.WS.demo) _XmlSerializer.Deserialize(_MemoryStream);

//Move data of the Typed class to a new instance of the demo DataSet
demo _demo2 = new demo();
System.IO.MemoryStream _MemoryStream2 = new System.IO.MemoryStream ();
_XmlSerializer.Serialize (_MemoryStream2,_TypedDemo);
_MemoryStream2.Position =0;
_demo2.ReadXml(_MemoryStream2);

I metodi di mappatura da dataset ad oggetto tipizzato e viceversa possono essere facilemente essere resi generici. Qui di seguito abbiamo per esempio il metodo per travasare i dati dal dataset alla sua corrispondente classe tipizzata.

public object fromdstoobj (Type thetype, DataSet theds) {
 System.Xml.Serialization.XmlSerializer _XmlSerializer
  = new System.Xml.Serialization.XmlSerializer(thetype);
 System.IO.MemoryStream _MemoryStream = new System.IO.MemoryStream ();
 foreach(DataRelation _rel in theds.Relations) 
      _rel.Nested =true;
 theds.WriteXml(_MemoryStream);
 _MemoryStream.Position=0;
 return _XmlSerializer.Deserialize(_MemoryStream);
}

Notare che occorre impostare la proprietà Nested delle relazioni tra tabelle a True affinchè l'XML emesso rispetti la gerarchia espressa dalle relazioni (se non si fa ciò i dati di ogni tabella sono emessi in blocchi indipendenti)

6/14/2005 11:49:11 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 
 
Feed di Blog2theMax
RSS 2.0 | Atom 0.2
Cerca nel blog
Archivio
<March 2010>
SunMonTueWedThuFriSat
28123456
78910111213
14151617181920
21222324252627
28293031123
45678910
Categorie

Powered by: newtelligence dasBlog 1.7.5016.2

 ©2004-2005 Code Architects S.r.l. - All rights reserved  Sign In