8. Eventi utente
Nel capitolo precedente abbiamo affrontato il concetto di eventi legati ai componenti del modulo. Vediamo ora come creare eventi nelle nostre classi.
8.1. Oggetti delegate predefiniti
Il concetto di oggetto delegate è stato introdotto nel capitolo precedente, ma all’epoca era stato trattato solo superficialmente. Quando abbiamo esaminato come venivano dichiarati i gestori degli eventi dei componenti di un modulo, abbiamo incontrato un codice simile al seguente:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
dove buttonAfficher era un componente di tipo [Button]. Questa classe ha un campo Click definito come segue:
![]() |
- [1]: la classe [Button]
- [2]: i suoi eventi
- [3,4]: l'evento Click
- [5]: la dichiarazione dell'evento [Control.Click] [4].
- EventHandler è un prototipo (un modello) di metodo denominato delegato.
- event è una parola chiave che limita le funzionalità di delegate e EventHandler: un oggetto delegate ha funzionalità più avanzate rispetto a un oggetto event.
Il delegate EventHandler è definito come segue:
![]() |
Il delegate EventHandler indica un modello di metodo:
- avente come primo parametro un tipo Object
- avente come secondo parametro un tipo EventArgs
- che non restituisce alcun risultato
Un metodo corrispondente al modello definito da EventHandler potrebbe essere il seguente:
private void buttonAfficher_Click(object sender, EventArgs e);
Per creare un oggetto di tipo EventHandler, si procede come segue:
EventHandler evtHandler=new EventHandler(méthode correspondant au prototype défini par le type EventHandler);
Si potrà quindi scrivere:
Una variabile di tipo delegate è in realtà un elenco di riferimenti a metodi corrispondenti al modello delegate. Per aggiungere un nuovo metodo M alla variabile evtHandler sopra indicata, si utilizza la sintassi:
La notazione += può essere utilizzata anche se evtHandler è un elenco vuoto.
L'istruzione:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
aggiunge un metodo di tipo EventHandler all'elenco dei metodi dell'evento buttonAfficher.Click. Quando si verifica l'evento Click sul componente buttonAfficher, VB esegue l'istruzione:
dove:
- source è il componente di tipo object all'origine dell'evento
- evt di tipo EventArgs e non contiene informazioni
Tutti i metodi di firma void M(object,EventArgs) che sono stati associati all’evento Click tramite:
this.buttonAfficher.Click += new System.EventHandler(M);
saranno richiamati con i parametri (source, evt) trasmessi da VB.
8.2. Definizione degli oggetti delegate
L'istruzione
public delegate int Opération(int n1, int n2);
definisce un tipo denominato Opération che rappresenta un prototipo di funzione che accetta due interi e restituisce un intero. È la parola chiave delegate che rende Opération una definizione di prototipo di funzione.
Una variabile op di tipo Opération avrà il compito di memorizzare un elenco di funzioni corrispondenti al prototipo Opération:
La registrazione di un metodo fi nella variabile op avviene tramite op=new Operazione(fi) o, più semplicemente, con op=fi. Per aggiungere un metodo fj all’elenco delle funzioni già registrate, si scrive op+= fj. Per rimuovere un metodo fk già registrato, si scrive op-=fk. Se nel nostro esempio si digita n=op(n1,n2), tutti i metodi registrati nella variabile op verranno eseguiti con i parametri n1 e n2. Il risultato n ottenuto sarà quello dell’ultimo metodo eseguito. Non è possibile ottenere i risultati prodotti dall’insieme dei metodi. Per questo motivo, se si registra un elenco di metodi in una funzione delegata, questi restituiscono molto spesso un risultato di tipo void.
Consideriamo il seguente esempio:
using System;
namespace Chap6 {
class Class1 {
// definizione di un prototipo di funzione
// accetta 2 interi come parametri e restituisce un intero
public delegate int Opération(int n1, int n2);
// due metodi di istanza corrispondenti al prototipo
public int Ajouter(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}//aggiungere
public int Soustraire(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}//sottrarre
// un metodo statico corrispondente al prototipo
public static int Augmenter(int n1, int n2) {
Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
return n1 + 2 * n2;
}//aumentare
static void Main(string[] args) {
// si definisce un oggetto di tipo operazione per registrarvi delle funzioni
// si registra la funzione statica "aumentare"
Opération op = Augmenter;
// si esegue il delegato
int n = op(4, 7);
Console.WriteLine("n=" + n);
// creazione di un oggetto c1 di tipo class1
Class1 c1 = new Class1();
// si registra nel delegato il metodo «aggiungere» di c1
op = c1.Ajouter;
// esecuzione dell'oggetto delegato
n = op(2, 3);
Console.WriteLine("n=" + n);
// si registra nel delegato il metodo "sottrai" di c1
op = c1.Soustraire;
n = op(2, 3);
Console.WriteLine("n=" + n);
//Registrazione di due funzioni nel delegato
op = c1.Ajouter;
op += c1.Soustraire;
// esecuzione dell'oggetto delegato
op(0, 0);
// rimozione di una funzione dal delegato
op -= c1.Soustraire;
// si esegue il delegato
op(1, 1);
}
}
}
- riga 3: definisce una classe Class1.
- riga 6: definizione di delegate Opération: un prototipo di metodi che accetta due parametri di tipo int e restituisce un risultato di tipo int
- righe 9-12: il metodo di istanza Ajouter ha la firma del delegato Opération.
- righe 14-17: il metodo di istanza Soustraire ha la firma del delegato Opération.
- righe 20-23: il metodo di classe Augmenter ha la firma del delegato Opération.
- riga 25: il metodo Main è stato eseguito
- riga 20: la variabile op è di tipo delegato Operazione. Conterrà un elenco di metodi con la firma del tipo delegato Operazione. Le viene assegnato un primo riferimento a un metodo, ovvero quello relativo al metodo statico Class1.Augmenter.
- riga 31: viene eseguito il metodo delegate op: verranno eseguiti tutti i metodi a cui fa riferimento op. Verranno eseguiti con i parametri passati a delegate e op.. In questo caso, verrà eseguito solo il metodo statico Class1.Augmenter.
- riga 35: viene creata un’istanza c1 della classe Class1.
- riga 37: il metodo di istanza c1.Ajouter viene assegnato al delegato op. Augmenter era un metodo statico, Ajouter è un metodo di istanza. Abbiamo voluto dimostrare che ciò non ha alcuna importanza.
- riga 39: viene eseguito il metodo delegate op: il metodo Ajouter verrà eseguito con i parametri passati a delegate op.
- riga 42: si ripete la stessa operazione con il metodo di istanza Soustraire.
- righe 46-47: si inseriscono i metodi Ajouter e Soustraire nei metodi delegate e op.
- riga 49: viene eseguito il delegate op: i due metodi Ajouter e Soustraire verranno eseguiti con i parametri passati a delegate e op.
- riga 51: il metodo Soustraire viene rimosso da delegate e op.
- riga 53: viene eseguito il delegate op: verrà eseguito il metodo rimanente Ajouter.
I risultati dell'esecuzione sono i seguenti:
8.3. Delegati o interfacce?
I concetti di delegates e di interfaccia possono sembrare piuttosto simili e ci si può chiedere quali siano esattamente le differenze tra questi due concetti. Prendiamo il seguente esempio, simile a uno già studiato:
using System;
namespace Chap6 {
class Program1 {
// definizione di un prototipo di funzione
// accetta 2 interi come parametri e restituisce un intero
public delegate int Opération(int n1, int n2);
// due metodi di istanza corrispondenti al prototipo
public static int Ajouter(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}//aggiungere
public static int Soustraire(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}//sottrarre
// Esecuzione di un delegato
public static int Execute(Opération op, int n1, int n2){
return op(n1, n2);
}
static void Main(string[] args) {
// Esecuzione del delegato Aggiungi
Console.WriteLine(Execute(Ajouter, 2, 3));
// Esecuzione del delegato Sottrai
Console.WriteLine(Execute(Soustraire, 2, 3));
// Esecuzione di un delegato multicast
Opération op = Ajouter;
op += Soustraire;
Console.WriteLine(Execute(op, 2, 3));
// Rimozione di una funzione dal delegato
op -= Soustraire;
// si esegue il delegato
Console.WriteLine(Execute(op, 2, 3));
}
}
}
Alla riga 20, il metodo Execute richiede un riferimento a un oggetto del tipo delegato Opération definito alla riga 6. Ciò consente di passare al metodo Execute diversi metodi (righe 26, 28, 32 e 36). Questa proprietà di polimorfismo può essere ottenuta anche con un’interfaccia:
using System;
namespace Chap6 {
// interfaccia IOperation
public interface IOperation {
int operation(int n1, int n2);
}
// classe Aggiungi
public class Ajouter : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}
}
// classe Sottrai
public class Soustraire : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}
}
// classe di test
public static class Program2 {
// Esecuzione del metodo unico dell'interfaccia IOperation
public static int Execute(IOperation op, int n1, int n2) {
return op.operation(n1, n2);
}
public static void Main() {
// Esecuzione del delegato Aggiungi
Console.WriteLine(Execute(new Ajouter(), 2, 3));
// Esecuzione del delegato "Sottrai"
Console.WriteLine(Execute(new Soustraire(), 2, 3));
}
}
}
- righe 6-8: l’interfaccia [IOperation] definisce un metodo operation.
- righe 11-16 e 19-24: le classi [Ajouter] e [Soustraire] implementano l'interfaccia [IOperation].
- righe 29-31: il metodo Execute, il cui primo parametro è del tipo dell'interfaccia IOperation. Il metodo Execute riceverà in successione come primo parametro un'istanza della classe Ajouter e poi un'istanza della classe Soustraire.
Si riscontra chiaramente l’aspetto polimorfico che aveva il parametro di tipo delegate dell’esempio precedente. I due esempi mostrano al contempo le differenze tra questi due concetti.
I tipi delegate e interface sono intercambiabili
- se l’interfaccia ha un solo metodo. Infatti, il tipo delegate è un involucro per un unico metodo, mentre l’interfaccia può definire più metodi.
- se l’aspetto multicast del delegate non viene utilizzato. Questo concetto di multicast, infatti, non esiste nell’interfaccia.
Se entrambe queste condizioni sono soddisfatte, è possibile scegliere tra le due seguenti firme per il metodo Execute:
int Execute(IOperation op, int n1, int n2)
int Execute(Opération op, int n1, int n2)
La seconda, che utilizza il delegate, può rivelarsi più flessibile nell’uso. Infatti, nella prima firma, il primo parametro del metodo deve implementare l’interfaccia IOperation. Ciò richiede la creazione di una classe in cui definire il metodo da passare come primo parametro al metodo Execute. Nella seconda firma, qualsiasi metodo esistente con la firma corretta è idoneo. Non è necessaria alcuna implementazione aggiuntiva.
8.4. Gestione degli eventi
Gli oggetti delegate possono essere utilizzati per definire eventi. Una classe C1 può definire un evento evt nel modo seguente:
- un tipo delegate viene definito all’interno o all’esterno della classe C1:
- la classe C1 definisce un campo di tipo delegate Evt:
- quando un'istanza c1 della classe C1 vorrà segnalare un evento, eseguirà il proprio delegato Evt1 passando gli parametri definiti dal delegato Evt. Tutti i metodi registrati in delegate e Evt1 verranno quindi eseguiti con questi parametri. Si può dire che siano stati notificati dell’evento Evt1.
- Se un oggetto c2 che utilizza un oggetto c1 desidera essere informato del verificarsi dell’evento Evt1 sull’oggetto c1, registrerà uno dei suoi metodi c2.M nell’oggetto delegato c1.Evt1 dell’oggetto c1.. In questo modo il suo metodo c2.M verrà eseguito ogni volta che l’evento Evt1 si verificherà sull'oggetto c1. Potrà inoltre disiscriversi quando non vorrà più ricevere notifiche relative all'evento.
- Poiché l’oggetto delegato c1.Evt1 può registrare più metodi, diversi oggetti ci potranno registrarsi presso il delegato c1.Evt1 per essere avvisati dell'evento Evt1 su c1.
In questo scenario, abbiamo:
- una classe che segnala un evento
- delle classi che vengono informate di tale evento. Si dice che esse si iscrivano all’evento o si abbonino all’evento.
- un tipo delegate che definisce la firma dei metodi che riceveranno la notifica dell’evento
Il framework .NET definisce:
- una firma standard del delegate relativa a un evento
- source: l'oggetto che ha segnalato l'evento
- evtInfo: un oggetto di tipo EventArgs o derivato che fornisce informazioni sull'evento
- il nome del delegate deve terminare con EventHandler
- un modo standard per dichiarare un evento di tipo MyEventHandler in una classe:
Il campo Evt1 è di tipo delegate. La parola chiave event serve a limitare le operazioni che è possibile eseguire su di esso:
- dall’esterno della classe C1, sono possibili solo le operazioni += e -=. Ciò impedisce la rimozione (ad esempio a causa di un errore dello sviluppatore) dei metodi iscritti all’evento. È possibile semplicemente iscriversi (+=) o disiscriversi (-=) dall’evento.
- solo un'istanza di tipo C1 può eseguire la chiamata Evt1(source,evtInfo) che attiva l'esecuzione dei metodi iscritti all'evento Evt1.
Il framework .NET fornisce un metodo generico che soddisfa la firma di delegate di un evento:
public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
- il delegate EventHandler utilizza il tipo generico TEventArgs, che è il tipo del suo secondo parametro
- il tipo TEventArgs deve derivare dal tipo EventsArgs (dove TEventArgs: EventArgs)
Con questo tipo generico delegate, la dichiarazione di un evento X nella classe C seguirà lo schema consigliato di seguito:
- definire un tipo XEventArgs derivato da EventArgs per incapsulare le informazioni relative all’evento X
- definire nella classe C un evento di tipo EventHandler<XEventArgs>.
- definire nella classe C un metodo protetto
destinato a "pubblicare" l'evento X agli abbonati.
Consideriamo il seguente esempio:
- una classe Emetteur incapsula una temperatura. Questa temperatura viene monitorata. Quando tale temperatura supera una certa soglia, deve essere generato un evento. Chiameremo questo evento TemperatureTropHaute. Le informazioni relative a questo evento saranno incapsulate in un tipo TemperatureTropHauteEventArgs.
- Una classe Souscripteur si abbona all’evento precedente. Quando viene avvisata dell’evento, visualizza un messaggio sulla console.
- Un programma da console crea un emittente e due abbonati. Inserisce le temperature tramite la tastiera e le registra in un’istanza di tipo Emetteur. Se la temperatura è troppo alta, l’istanza Emetteur pubblica l’evento TemperatureTropHaute.
Per attenersi al metodo consigliato di gestione degli eventi, definiamo innanzitutto il tipo TemperatureTropHauteEventArgs per incapsulare le informazioni relative all’evento:
using System;
namespace Chap6 {
public class TemperatureTropHauteEventArgs:EventArgs {
// temperatura al momento dell'evento
public decimal Temperature { get; set; }
// costruttori
public TemperatureTropHauteEventArgs() {
}
public TemperatureTropHauteEventArgs(decimal temperature) {
Temperature = temperature;
}
}
}
- riga 6: l’informazione incapsulata dalla classe TemperatureTropHauteEventArgs è la temperatura che ha provocato l’evento TemperatureTropHaute.
La classe Emetteur è la seguente:
using System;
namespace Chap6 {
public class Emetteur {
static decimal SEUIL = 19;
// temperatura rilevata
private decimal temperature;
// nome della fonte
public string Nom { get; set; }
// evento segnalato
public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;
// lettura/scrittura della temperatura
public decimal Temperature {
get {
return temperature;
}
set {
temperature = value;
if (temperature > SEUIL) {
// l'evento viene segnalato agli abbonati
OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
}
}
}
// segnalazione di un evento
protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
// invio dell'evento TemperatureTropHaute agli abbonati
TemperatureTropHaute(this, evt);
}
}
}
- riga 5: la soglia di temperatura oltre la quale verrà pubblicato l'evento TemperatureTropHaute.
- riga 10: il mittente ha un nome per essere identificato
- riga 12: l'evento TemperatureTropHaute.
- righe 15-26: il metodo get che restituisce la temperatura e il metodo set che la registra. È il metodo set che fa pubblicare l’evento TemperatureTropHaute se la temperatura da registrare supera la soglia della riga 5. Provoca la pubblicazione dell’evento tramite il metodo OnTemperatureTropHauteHandler della riga 29, passando come parametro un oggetto TemperatureTropHauteEventArgs in cui è stata registrata la temperatura che ha superato la soglia.
- righe 29-32: l’evento TemperatureTropHaute viene pubblicato con come primo parametro il trasmettitore stesso e come secondo parametro l’oggetto TemperatureTropHauteEventArgs ricevuto come parametro.
La classe Souscripteur che si abbonerà all’evento TemperatureTropHaute è la seguente:
using System;
namespace Chap6 {
public class Souscripteur {
// nome
public string Nom { get; set; }
// gestore dell'evento TemperatureTropHaute
public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
// visualizzazione console operatore
Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
}
}
}
- riga 6: ogni sottoscrittore è identificato da un nome.
- righe 9-12: il metodo che verrà associato all'evento TemperatureTropHaute. Ha la firma del tipo delegate EventHandler<TEventArgs> che un gestore di eventi deve avere. Il metodo visualizza sulla console: il nome del sottoscrittore che visualizza il messaggio, il nome dell’emittente che ha segnalato l’evento, la temperatura che lo ha attivato.
- L'iscrizione all'evento TemperatureTropHaute di un oggetto Emetteur non viene effettuata nella classe Souscripteur. Sarà effettuata da una classe esterna.
Il programma [Program.cs] collega tra loro tutti questi elementi:
using System;
namespace Chap6 {
class Program {
static void Main(string[] args) {
// creazione di un generatore di eventi
Emetteur e1 = new Emetteur() { Nom = "e" };
// creazione di un array di 2 sottoscrittori
Souscripteur[] souscripteurs = new Souscripteur[2];
for (int i = 0; i < souscripteurs.Length; i++) {
// creazione di un sottoscrittore
souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
// lo si abbona all'evento TemperatureTropHaute di e1
e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
}
// si leggono le temperature dalla tastiera
decimal temperature;
Console.Write("Température (rien pour arrêter) : ");
string saisie = Console.ReadLine().Trim();
// finché la riga inserita non è vuota
while (saisie != "") {
// il valore inserito è un numero decimale?
if (decimal.TryParse(saisie, out temperature)) {
// temperatura corretta - la si registra
e1.Temperature = temperature;
} else {
// si segnala l'errore
Console.WriteLine("Température incorrecte");
}
// nuovo inserimento
Console.Write("Température (rien pour arrêter) : ");
saisie = Console.ReadLine().Trim();
}//while
}
}
}
- riga 6: creazione dell'emittente
- righe 8-14: creazione di due sottoscrittori che vengono iscritti all'evento TemperatureTropHaute dell'emittente.
- righe 20-32: ciclo di immissione delle temperature tramite tastiera
- riga 24: se la temperatura inserita è corretta, viene trasmessa all’oggetto Emetteur e1 che attiverà l’evento TemperatureTropHaute se la temperatura è superiore a 19 °C.
I risultati dell'esecuzione sono i seguenti:

