Evenimente
Evenimentele semnalizează sistemului că a avut loc o anumită acțiune. Și dacă trebuie să urmărim aceste acțiuni, putem folosi evenimentele.
De exemplu, să luăm următoarea clasă care descrie un cont bancar:
class Account
{
// suma din cont
public int Sum { get; private set; }
// în constructor se stabilește suma inițială din cont
public Account(int sum) => Sum = sum;
// adăugarea de fonduri în cont
public void Put(int sum) => Sum += sum;
// retragerea de fonduri din cont
public void Take(int sum)
{
if (Sum >= sum)
{
Sum -= sum;
}
}
}
În constructor se stabilește suma inițială, care este păstrată în proprietatea Sum. Cu ajutorul metodei Put putem adăuga fonduri în cont, iar cu ajutorul metodei Take, dimpotrivă, retragem bani din cont. Să încercăm să folosim clasa în program - să creăm un cont, să depunem și să retragem bani din el:
Account account = new Account(100);
account.Put(20); // adăugăm în cont 20
Console.WriteLine($"Suma în cont: {account.Sum}");
account.Take(70); // încercăm să retragem din cont 70
Console.WriteLine($"Suma în cont: {account.Sum}");
account.Take(180); // încercăm să retragem din cont 180
Console.WriteLine($"Suma în cont: {account.Sum}");
Ieșirea în consolă:
Suma în cont: 120
Suma în cont: 50
Suma în cont: 50
Toate operațiunile funcționează așa cum ar trebui. Dar ce se întâmplă dacă dorim să notificăm utilizatorul despre rezultatele operațiunilor sale? Am putea, de exemplu, să modificăm metoda Put astfel:
public void Put(int sum)
{
Sum += sum;
Console.WriteLine($"În cont s-a depus: {sum}");
}
Acum vom fi notificați despre operațiune, văzând mesajul corespunzător în consolă. Dar există câteva observații. În momentul definirii clasei, este posibil să nu știm exact ce acțiune dorim să realizăm în metoda Put ca răspuns la adăugarea de bani. Poate fi afișare în consolă, sau poate dorim să notificăm utilizatorul prin email sau sms.
Mai mult, putem crea o bibliotecă separată de clase care să conțină această clasă și să o adăugăm în alte proiecte. Și deja din aceste proiecte să decidem ce acțiune trebuie realizată. Poate dorim să folosim clasa Account într-o aplicație grafică și să afișăm la adăugarea în cont într-un mesaj grafic, nu în consolă.
Sau biblioteca noastră de clase va fi utilizată de un alt dezvoltator, care are propria opinie despre ce trebuie să facă la adăugarea în cont. Toate aceste întrebări le putem rezolva utilizând evenimentele.
Definirea și apelarea evenimentelor
Evenimentele sunt declarate în clasă cu ajutorul cuvântului cheie event, după care se specifică tipul delegatului care reprezintă evenimentul:
delegate void AccountHandler(string message);
event AccountHandler Notify;
În acest caz, mai întâi se definește delegatul AccountHandler, care primește un parametru de tip string. Apoi, cu ajutorul cuvântului cheie event, se definește evenimentul cu numele Notify, care reprezintă delegatul AccountHandler. Numele pentru eveniment poate fi arbitrar, dar în orice caz trebuie să reprezinte un delegat.
Definind evenimentul, îl putem apela în program ca o metodă, folosind numele evenimentului:
Notify("A avut loc o acțiune");
Deoarece evenimentul Notify reprezintă delegatul AccountHandler, care primește un parametru de tip string, la apelarea evenimentului trebuie să transmitem o valoare de tip string.
Cu toate acestea, la apelarea evenimentelor, ne putem confrunta cu faptul că evenimentul este null în cazul în care nu este definit un handler. Prin urmare, la apelarea evenimentului, este mai bine să îl verificăm întotdeauna pentru null. De exemplu, astfel:
if(Notify != null) Notify("A avut loc o acțiune");
Sau astfel:
Notify?.Invoke("A avut loc o acțiune");
În acest caz, deoarece evenimentul reprezintă un delegat, îl putem apela folosind metoda Invoke(), transmițând valorile necesare pentru parametri.
Să combinăm totul și să creăm și să apelăm un eveniment:
class Account
{
public delegate void AccountHandler(string message);
public event AccountHandler? Notify; // 1.Definirea evenimentului
public Account(int sum) => Sum = sum;
public int Sum { get; private set; }
public void Put(int sum)
{
Sum += sum;
Notify?.Invoke($"În cont s-a depus: {sum}"); // 2.Apelarea evenimentului
}
public void Take(int sum)
{
if (Sum >= sum)
{
Sum -= sum;
Notify?.Invoke($"Din cont s-a retras: {sum}"); // 2.Apelarea evenimentului
}
else
{
Notify?.Invoke($"Nu sunt suficienți bani în cont. Sold curent: {Sum}");
}
}
}
Acum, cu ajutorul evenimentului Notify, notificăm sistemul că au fost adăugate fonduri și că fondurile au fost retrase din cont sau că nu sunt suficiente fonduri în cont.
Adăugarea handler-ului de eveniment
Cu evenimentul poate fi asociat unul sau mai mulți handler-i. Handler-ii de eveniment sunt exact ceea ce se execută la apelarea evenimentelor. De multe ori, ca handler-i de eveniment se folosesc metode.
Fiecare handler de eveniment, după lista de parametri și tipul de returnare, trebuie să corespundă delegatului care reprezintă evenimentul. Pentru adăugarea unui handler de eveniment se folosește operația +=:
Notify += handler de eveniment;
Să definim handler-i pentru evenimentul Notify pentru a primi notificările dorite în program:
Account account = new Account(100);
account.Notify += DisplayMessage; // Adăugăm handler pentru evenimentul Notify
account.Put(20); // adăugăm în cont 20
Console.WriteLine($"Suma în cont: {account.Sum}");
account.Take(70); // încercăm să retragem din cont 70
Console.WriteLine($"Suma în cont: {account.Sum}");
account.Take(180); // încercăm să retragem din cont 180
Console.WriteLine($"Suma în cont: {account.Sum}");
void DisplayMessage(string message) => Console.WriteLine(message);
În acest caz, ca handler se folosește metoda DisplayMessage, care corespunde după lista de parametri și tipul de returnare delegatului AccountHandler. În final, la apelarea evenimentului Notify?.Invoke() se va apela metoda DisplayMessage, căreia pentru parametrul message i se va transmite șirul transmis la Notify?.Invoke().
În DisplayMessage, pur și simplu afișăm mesajul primit de la eveniment, dar am putea defini orice logică.
Dacă în acest caz handler-ul nu ar fi fost setat, atunci la apelarea evenimentului Notify?.Invoke() nu s-ar fi întâmplat nimic, deoarece evenimentul Notify ar fi fost egal cu null.
Ieșirea în consolă a programului:
În cont s-a depus: 20
Suma în cont: 120
Din cont s-a retras: 70
Suma în cont: 50
Nu sunt suficienți bani în cont. Sold curent: 50
Suma în cont: 50
Acum putem să evidențiem clasa Account într-o bibliotecă separată de clase și să o adăugăm în orice proiect.
Adăugarea și eliminarea handler-ilor
Pentru un eveniment se pot seta mai mulți handler-i și apoi, în orice moment, se pot elimina. Pentru eliminarea handler-ilor se folosește operația -=. De exemplu:
Account account = new Account(100);
account.Notify += DisplayMessage; // adăugăm handler-ul DisplayMessage
account.Notify += DisplayRedMessage; // adăugăm handler-ul Display
RedMessage
account.Put(20); // adăugăm în cont 20
account.Notify -= DisplayRedMessage; // eliminăm handler-ul DisplayRedMessage
account.Put(50); // adăugăm în cont 50
void DisplayMessage(string message) => Console.WriteLine(message);
void DisplayRedMessage(string message)
{
// Setăm culoarea roșie pentru caractere
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(message);
// Resetăm setările de culoare
Console.ResetColor();
}
Ieșirea în consolă:
În cont s-a depus: 20
În cont s-a depus: 20
În cont s-a depus: 50
Ca handler-i se pot folosi nu doar metode obișnuite, ci și delegați, metode anonime și lambda-uri. Utilizarea delegaților și metodelor:
Account acc = new Account(100);
// setarea delegatului, care indică spre metoda DisplayMessage
acc.Notify += new Account.AccountHandler(DisplayMessage);
// setarea metodei DisplayMessage ca handler
acc.Notify += DisplayMessage; // adăugăm handler-ul DisplayMessage
acc.Put(20); // adăugăm în cont 20
void DisplayMessage(string message) => Console.WriteLine(message);
În acest caz, nu va exista nicio diferență între cei doi handler-i.
Setarea unei metode anonime ca handler:
Account acc = new Account(100);
acc.Notify += delegate (string mes)
{
Console.WriteLine(mes);
};
acc.Put(20);
Setarea unei lambda ca handler:
Account account = new Account(100);
account.Notify += message => Console.WriteLine(message);
account.Put(20);
Gestionarea handler-ilor
Cu ajutorul accesoriilor speciale add/remove, putem gestiona adăugarea și eliminarea handler-ilor. De obicei, această funcționalitate este rar necesară, dar totuși o putem folosi. De exemplu:
class Account
{
public delegate void AccountHandler(string message);
AccountHandler? notify;
public event AccountHandler Notify
{
add
{
notify += value;
Console.WriteLine($"{value.Method.Name} adăugat");
}
remove
{
notify -= value;
Console.WriteLine($"{value.Method.Name} eliminat");
}
}
public Account(int sum) => Sum = sum;
public int Sum { get; private set; }
public void Put(int sum)
{
Sum += sum;
notify?.Invoke($"În cont s-a depus: {sum}"); // 2.Apelarea evenimentului
}
public void Take(int sum)
{
if (Sum >= sum)
{
Sum -= sum;
notify?.Invoke($"Din cont s-a retras: {sum}"); // 2.Apelarea evenimentului
}
else
{
notify?.Invoke($"Nu sunt suficienți bani în cont. Sold curent: {Sum}");
}
}
}
Acum definirea evenimentului se împarte în două părți. Mai întâi se definește o variabilă delegat prin care putem apela handler-ii asociați:
AccountHandler notify;
În a doua parte, definim accesoriile add și remove. Accesoriul add este apelat la adăugarea unui handler, adică la operația +=. Handler-ul adăugat este accesibil prin cuvântul cheie value. Aici putem obține informații despre handler (de exemplu, numele metodei prin value.Method.Name) și defini o anumită logică. În acest caz, pentru simplitate, doar afișăm un mesaj în consolă:
add
{
notify += value;
Console.WriteLine($"{value.Method.Name} adăugat");
}
Blocul remove este apelat la eliminarea unui handler. În mod similar, putem defini o logică suplimentară:
remove
{
notify -= value;
Console.WriteLine($"{value.Method.Name} eliminat");
}
În interiorul clasei, evenimentul este apelat tot prin variabila notify. Dar pentru adăugarea și eliminarea handler-ilor în program se folosește chiar Notify:
Account acc = new Account(100);
acc.Notify += DisplayMessage; // adăugăm handler-ul DisplayMessage
acc.Put(20); // adăugăm în cont 20
acc.Notify -= DisplayMessage; // eliminăm handler-ul DisplayMessage
acc.Put(20); // adăugăm în cont 20
void DisplayMessage(string message) => Console.WriteLine(message);
Ieșirea în consolă a programului:
DisplayMessage adăugat
În cont s-a depus: 20
DisplayMessage eliminat
Transmiterea datelor evenimentului
Adesea, la apariția unui eveniment, handler-ului evenimentului i se cere să transmită anumite informații despre eveniment. De exemplu, să adăugăm în programul nostru o nouă clasă AccountEventArgs cu următorul cod:
class AccountEventArgs
{
// Mesaj
public string Message { get; }
// Suma cu care s-a modificat contul
public int Sum { get; }
public AccountEventArgs(string message, int sum)
{
Message = message;
Sum = sum;
}
}
Această clasă are două proprietăți: Message - pentru a stoca mesajul afișat și Sum - pentru a stoca suma cu care s-a modificat contul.
Acum folosim clasa AccountEventArgs, modificând clasa Account astfel:
class Account
{
public delegate void AccountHandler(Account sender, AccountEventArgs e);
public event AccountHandler? Notify;
public int Sum { get; private set; }
public Account(int sum) => Sum = sum;
public void Put(int sum)
{
Sum += sum;
Notify?.Invoke(this, new AccountEventArgs($"În cont s-a depus {sum}", sum));
}
public void Take(int sum)
{
if (Sum >= sum)
{
Sum -= sum;
Notify?.Invoke(this, new AccountEventArgs($"Suma {sum} retrasă din cont", sum));
}
else
{
Notify?.Invoke(this, new AccountEventArgs("Nu sunt suficienți bani în cont", sum));
}
}
}
Comparativ cu versiunea anterioară a clasei Account, aici s-a modificat doar numărul de parametri ai delegatului și, în consecință, numărul de parametri la apelarea evenimentului. Acum delegatul AccountHandler primește ca prim parametru obiectul care a declanșat evenimentul, adică obiectul curent Account.
Iar ca al doilea parametru primește obiectul AccountEventArgs, care stochează informații despre eveniment, obținută prin constructor.
Acum modificăm programul principal:
Account acc = new Account(100);
acc.Notify += DisplayMessage;
acc.Put(20);
acc.Take(70);
acc.Take(150);
void DisplayMessage(Account sender, AccountEventArgs e)
{
Console.WriteLine($"Suma tranzacției: {e.Sum}");
Console.WriteLine(e.Message);
Console.WriteLine($"Suma curentă în cont: {sender.Sum}");
}
Comparativ cu varianta anterioară, aici doar modificăm numărul de parametri și utilizarea lor în handler-ul DisplayMessage. Datorită primului parametru, în metodă putem obține informații despre emitentul evenimentului - contul cu care se realizează operația. Iar prin al doilea parametru putem obține informații despre starea operației.