lunedì 4 ottobre 2010

VB.NET for Dummies: Gli Eventi

Vorrei iniziare con questo post una serie dedicata ad aspetti di VB.NET di base che possono essere utile a coloro che si avvicinano al mondo .NET e che, in genere, non vengono trattati a livello base.

La serie di post non ha la pretesa di essere assolutamente esaustivi sugli argomenti che tratterò

In questo primo post parleremo degli eventi.

Cosa sono e a cosa servono

Un evento è la “notifica” dell’accadimento di qualcosa.

Quando, ad esempio, premiamo un bottone della finestra di un’applicazione, dietro le quinte, il bottone stesso “notifica” al mondo circostante che qualcuno, in quell’istante, lo ha premuto.

Sta, poi, al mondo circostante preoccuparsi di “intercettare” l’evento stesso per gestirlo (tramite un gestore di evento).

Attenzione a non confondere la pressione del tasto con la “notifica” della pressione del tasto: l’evento è la “notifica” dell’accadimento, non l’accadimento stesso.

Ma a cosa serve tutto ciò?

Utilizzare gli eventi è un modo per disaccoppiare due o più attori del nostro dominio.

Sempre tornando al bottone di cui sopra, infatti, lo stesso bottone non è assolutamente a conoscenza di chi gestirà la sua “notifica”. Semplicemente comunica tale notifica. Questo ci permette di far gestire l’evento del bottone da qualsiasi classe senza minimamente modificare il bottone stesso.

Tanto per fare un’altro esempio, immaginate una newsletter: l’invio della newsletter è un evento, se voi la leggete avete gestito l’evento!

Come si definisce un evento

Un evento si definisce utilizzando la seguente sintassi:

  1. Public Event <NomeEvento>(<parameterList>)

dove <NomeEvento> è il nome che vogliamo assegnare all’evento (ad esempio Click per la pressione di un Button) e <parameterList> è la lista di argomenti che vogliamo trasferire a chi si è sottoscritto per ricevere la “notifica”.

Lo standard utilizzato all’interno del framework è il seguente:

  1. Public Event <NomeEvento>(sender as object, e as MyArgs)

dove

  • sender è l’istanza della classe (oggetto) che ha sollevato l’evento;
  • MyArgs è una classe che deriva da System.EventArgs (o da sue derivate) che contiene l’argomento dell’evento e che ci serve per trasportare oggetti all’indirizzo di chi deve gestire l’evento.

Il sender tra gli argomenti permette, a chi si sottoscrive, di poter gestire gli eventi di più oggetti con lo stesso tipo di evento in un unico punto (gestore).

Per esemplificare ulteriormente cominciamo con il realizzare una classe che si occuperà di simulare un lavoro di lunga durata:

  1. Public Class LongRunJob
  2.  
  3.     Public Event JobCompleted(ByVal sender As Object, ByVal e As EventArgs)
  4.  
  5.     Public Sub DoWork(ByVal nSec As Integer)
  6.         .
  7.         .
  8.     End Sub
  9.     
  10. End Class

Abbiamo definito un evento che segnalerà il completamento del lavoro. Il metodo DoWork, come vedremo più avanti, si occuperà di avviare un thread che, banalmente, dopo nSec secondi segnalerà l’avvenuto completamento.

Come si solleva un evento

Per poter scatenare un evento è sufficiente eseguire l’istruzione:

  1. RaiseEvent <NomeEvento>(<ParameterList>)

In particolare, per sollevare l’evento JobCompleted() della classe LongRunJob, possiamo scrivere:

  1. RaiseEvent JobCompleted(Me, EventArgs.Empty)

Completiamo, quindi, la nostra classe LongRunJob con il thread che esegue il lavoro:

  1. Public Class LongRunJob
  2.  
  3.     Public Event JobCompleted(ByVal sender As Object, ByVal e As EventArgs)
  4.  
  5.     Public Sub DoWork(ByVal nSec As Integer)
  6.         Dim threadStart = New Threading.ParameterizedThreadStart(AddressOf DoWorkCode)
  7.         Dim thread = New Threading.Thread(threadStart)
  8.         thread.Start(nSec)
  9.     End Sub
  10.  
  11.     Private Sub DoWorkCode(ByVal data As Object)
  12.         Dim nSec = CType(data, Int32)
  13.         System.Threading.Thread.Sleep(nSec * 1000)
  14.         RaiseEvent JobCompleted(Me, EventArgs.Empty)
  15.     End Sub
  16.  
  17. End Class

Come si gestisce un evento

La nostra classe comunica, quindi, al mondo circostante di aver completato il proprio lavoro, ma come facciamo ad intercettare l’evento in un’altra classe?

Per poterci “sottoscrivere” alla ricezione di un evento utiliziamo la parola chiave AddHandler:

  1. AddHandler calc.JobCompleted, AddressOf LongRunJobCompletedHandler

In questo modo comunichiamo all’istanza della classe LongRunJob (vedremo più avanti in che modo) quale metodo deve essere richiamato nel momento in cui viene sollevato l’evento. E’ la classe che vuole essere informata della notifica che ha il dovere di sottoscriversi.

Il metodo che gestirà l’evento deve avere la stessa firma dell’evento stesso (Stessi parametri per tipo, ordine e numero).

Nel momento in cui non abbianmo più necessità di ricevere le notifiche dell’evento, possiamo “sganciare” il gestore di evento con la parola chiave RemoveHandler:

  1. RemoveHandler calc.JobCompleted, AddressOf LongRunJobCompletedHandler

Un esempio di gestione dell’evento è il seguente:

  1. Module Module1
  2.  
  3.     Sub Main()
  4.         Dim calc = New LongRunJob()
  5.         AddHandler calc.JobCompleted, AddressOf LongRunJobCompletedHandler
  6.         Console.WriteLine("Inizio lavoro!")
  7.         calc.DoWork(10)
  8.         Console.ReadLine()
  9.         RemoveHandler calc.JobCompleted, AddressOf LongRunJobCompletedHandler
  10.     End Sub
  11.  
  12.     Public Sub LongRunJobCompletedHandler(ByVal sender As Object, ByVal e As EventArgs)
  13.         Console.WriteLine("Lavoro completato!")
  14.     End Sub
  15.  
  16. End Module

Cosa c’è dietro le quinte

Perchè in C#, per implementare un evento, si deve definire un delegate, definire l’evento come membro della classe di tipo delegate e via dicendo mentre VB, essendo comunque un linguaggio del framework, non ha la necessità di scrivere tutto ciò?

La risposta è che il compilatore VB semplifica la vita e crea per noi la struttura del delegate che gestisce l’evento.

In particolare apriamo la nostra classe LongRunJob con Reflector e vediamo esattamente come si presenta:

imageOsserviamo che il compilatore, dietro le quinte, crea un delegate e definisce l’evento con lo stesso delegate:

  1. Public Delegate Sub JobCompletedEventHandler(ByVal sender As Object, ByVal e As EventArgs)
  2.  
  3. Public Custom Event JobCompleted As JobCompletedEventHandler

Inoltre, la parola chiave RaieseEvent (che viene da noi invocata senza controllare se c’è qualcuno in ascolto) viene compilata nel seguente modo:

  1. Private Sub DoWorkCode(ByVal data As Object)
  2.     Thread.Sleep(CInt((Conversions.ToInteger(data) * 1000)))
  3.     Dim VB$t_ref$S0 As JobCompletedDelegate = Me.JobCompletedEvent
  4.     If (Not VB$t_ref$S0 Is Nothing) Then
  5.         VB$t_ref$S0.Invoke(Me, EventArgs.Empty)
  6.     End If
  7. End Sub

IL compilatore, quindi, genera un codice del tutto simile a quello che scriverebbe un programmatore C#: si controlla se c’è qualcuno in ascolto e, solo in quel caso, si solleva l’evento (metodo Invoke).

Osserviamo, inoltre, che l’evento, nella classe compilata, viene generato come evento custom (ci occuperemo in dettaglio degli eventi custom in un prossimo post). Vengono generati i due metodi richiamati per l’aggancio del gestore di evento e per lo sgancio dello stesso:

  1. Public Sub add_JobCompleted(ByVal obj As JobCompletedEventHandler)
  2.     Me.JobCompletedEvent = DirectCast(Delegate.Combine(Me.JobCompletedEvent, obj), _
  3.         JobCompletedEventHandler)
  4. End Sub
  5.  
  6. Public Sub remove_JobCompleted(ByVal obj As JobCompletedEventHandler)
  7.     Me.JobCompletedEvent = DirectCast(Delegate.Remove(Me.JobCompletedEvent, obj), _
  8.         JobCompletedEventHandler)
  9. End Sub

Di fatto, i gestori di evento agganciati al nostro evento sono “memorizzati” nell’attributo privato JobCompletedEvent (generato dal compilatore). JobCompletedEvent è un oggetto di classe MultiCastDelegate, ovvero un oggetto in grado di avere collegata una lista di gestori di eventi (delegate) detta lista di invocazione. I metodi Combine e Remove del MulticastDelegate consentono di aggiungere e rimuovere un gestore di evento dalla lista di invocazione.

Ultima annotazione importante è la presenza dell’attributo JobCompletedEvent di tipo JobCompletedEventHandler. Questo è presente all’interno della nostra classe pur non essendo visibile all’intellisense. Di fatto possiamo utilizzarlo per capire se qualcuno si è agganciato al nostro evento:

  1. Public Function IsJobCompletedListening() As Boolean
  2.     Return (Me.JobCompletedEvent IsNot Nothing)
  3. End Function

Alla fine, sfruttando le osservazioni fatte, possiamo scrivere la nostra classe “alla C#”, definendo, cioè tutto quello che ci serve:

  1. Public Class LongRunJobCSLike
  2.  
  3.     Public Delegate Sub JobCompletedDelegate(ByVal sender As Object, ByVal e As EventArgs)
  4.  
  5.     Public Event JobCompleted As JobCompletedDelegate
  6.  
  7.     Public Sub DoWork(ByVal nSec As Integer)
  8.         Dim threadStart = New Threading.ParameterizedThreadStart(AddressOf DoWorkCode)
  9.         Dim thread = New Threading.Thread(threadStart)
  10.         thread.Start(nSec)
  11.     End Sub
  12.  
  13.     Private Sub DoWorkCode(ByVal data As Object)
  14.         Dim nSec = CType(data, Int32)
  15.         System.Threading.Thread.Sleep(nSec * 1000)
  16.         If Me.JobCompletedEvent IsNot Nothing Then
  17.             Me.JobCompletedEvent.Invoke(Me, EventArgs.Empty)
  18.         End If
  19.     End Sub
  20.  
  21. End Class

In questo caso il compilatore non definisce il delegate in quanto lo abbiamo già fatto noi.

Nessun commento: