1. Praktyczne scenariusze użycia zdarzeń
Zdarzenia — to nie tylko ładna teoria z podręczników. W praktyce są potrzebne prawie w co drugim aplikacji, a jeśli pracujesz z UI lub serwisami sieciowymi — to w większości przypadków.
Zobaczmy kilka typowych scenariuszy z rzeczywistego developmentu. Przy okazji sprawdzimy, jak standardy i najlepsze praktyki pomagają unikać popularnych błędów.
Reagujemy na działania użytkownika (jak w UI)
Może to być najczęstszy scenariusz — gdy użytkownik coś robi (klika przycisk, wybiera element na liście), i aplikacja musi zareagować. Tak działają Windows Forms, WPF, UWP, Avalonia, MAUI i inne frameworki UI.
Mini-przykład (bez UI — imitujemy przycisk):
public class Button
{
public event EventHandler? Click; // standardowy delegate
public void SimulateClick()
{
Click?.Invoke(this, EventArgs.Empty); // "Przycisk" został "kliknięty"
}
}
class Program
{
static void Main()
{
var button = new Button();
button.Click += (sender, e) => Console.WriteLine("Przycisk kliknięty!");
button.SimulateClick(); // > Przycisk kliknięty!
}
}
W prawdziwym frameworku UI zdarzenie Click wyzwala się, kiedy użytkownik klika myszką lub dotyka ekranu. Wszystko, co się na to zdarzenie zapisało, otrzyma powiadomienie — tak działa cała logika aplikacji.
Sygnały postępu, zakończenia operacji i błędów
Wyobraź sobie: pobieranie pliku, przetwarzanie danych, długotrwałe obliczenia. Trzeba informować o postępie albo o błędach. Często organizuje się zdarzenie na "krok postępu" i osobne zdarzenie na "zakończono" lub "błąd".
Przykład downloadera plików:
public class FileDownloader
{
public event EventHandler<int>? ProgressChanged;
public event EventHandler? DownloadCompleted;
public event EventHandler<string>? DownloadFailed;
public void Download()
{
for (int i = 1; i <= 100; i += 10)
{
Thread.Sleep(50); // imitacja opóźnienia
ProgressChanged?.Invoke(this, i);
}
DownloadCompleted?.Invoke(this, EventArgs.Empty);
}
}
var downloader = new FileDownloader();
downloader.ProgressChanged += (s, progress) => Console.WriteLine($"Pobieranie: {progress}%");
downloader.DownloadCompleted += (s, e) => Console.WriteLine("Pobieranie zakończone.");
downloader.Download();
Tu jest kilka zdarzeń: można subskrybować wszystko (albo tylko to, co potrzebne). To podobne do działania wielu standardowych klas .NET dla wątków, pobierania plików, HTTP itp.
Reakcja na cykl życia obiektów (np. "zapisano", "usunięto")
Załóżmy, że masz model domenowy. Chcesz, żeby gdy użytkownik zapisze lub usunie obiekt, jakieś procesy zareagowały: odświeżyły cache, zalogowały, wysłały wiadomość itp.
public class UserRepository
{
public event EventHandler<UserEventArgs>? UserSaved;
public event EventHandler<UserEventArgs>? UserDeleted;
public void Save(User user)
{
//... zapisujemy użytkownika
UserSaved?.Invoke(this, new UserEventArgs(user));
}
public void Delete(User user)
{
//... usuwamy użytkownika
UserDeleted?.Invoke(this, new UserEventArgs(user));
}
}
public class UserEventArgs : EventArgs
{
public User User { get; }
public UserEventArgs(User user) => User = user;
}
Teraz różne moduły mogą się zapisać i — bez bezpośrednich powiązań! — otrzymywać powiadomienia o zmianach użytkowników. Przy tym sam repository nie wie, kto "podłączył się".
Asynchroniczne zdarzenia i wielowątkowość
Czasami zdarzenia są generowane nie z głównego (UI) wątku — np. z zadań w tle, timerów lub operacji asynchronicznych. W takich przypadkach ważne jest pamiętanie, że kod handlerów wykona się w "obcym" wątku. Jeśli handler próbuje zaktualizować elementy interfejsu bezpośrednio z innego wątku — spowoduje to błędy.
Co to jest marshaling? Marshaling to przekazywanie wykonania kodu z jednego wątku do drugiego, zwykle z wątku tła z powrotem do UI‑wątku, aby bezpiecznie zaktualizować interfejs. W aplikacjach UI (WinForms, WPF) używa się mechanizmów jak SynchronizationContext lub Dispatcher, które pozwalają "przekierować" wywołanie handlera na odpowiedni wątek.
Nie rób tak:
// Zdarzenie jest wywoływane z wątku w tle, a handler aktualizuje UI bezpośrednio — dostaniesz wyjątek!
Zalecane: sprawdzaj wątek wykonania i w razie potrzeby rób marshaling na UI‑wątek (przez SynchronizationContext lub Dispatcher). W aplikacjach konsolowych i serwerowych takich ograniczeń zazwyczaj nie ma.
Event Aggregator / Messaging
W dużych aplikacjach często używa się scentralizowanego agregatora zdarzeń (Event Aggregator). Zmniejsza to powiązania: subskrybenci i wydawcy nie znają się nawzajem i wymieniają komunikaty przez centrum.
public class EventAggregator
{
public event EventHandler<SomeEventArgs>? SomeEvent;
public void Publish(SomeEventArgs args)
{
SomeEvent?.Invoke(this, args);
}
}
2. Najlepsze wzorce i praktyki
Używaj atrybutu [CallerMemberName] dla błędów
Jeśli tworzysz kod infrastrukturalny (np. loggery, tracery), loguj nazwę metody‑źródła zdarzenia za pomocą atrybutu CallerMemberName — to upraszcza diagnostykę.
Staraj się robić zdarzenia thread-safe, jeśli potrzeba
Zdarzenia są multicastowe, a dostęp z różnych wątków wymaga ostrożności: przed wywołaniem skopiuj delegat do zmiennej lokalnej.
var handler = MyEvent;
if (handler != null) handler(this, args);
W C# 6+ używaj bezpiecznego wywołania: MyEvent?.Invoke(this, args).
Projektuj własne EventArgs jako immutable
Definiuj własne typy EventArgs z właściwościami tylko do odczytu — to zapobiega przypadkowemu modyfikowaniu stanu zdarzenia przez subskrybentów.
public class WorkCompletedEventArgs : EventArgs
{
public string Message { get; }
public WorkCompletedEventArgs(string message) => Message = message;
}
public czy private event
Rób zdarzenia public event, a wywołanie enkapsuluj w protected virtual metodzie OnEventName. To daje rozszerzalność (klasa pochodna może nadpisać zachowanie), a zewnętrzni konsumenci nie będą mogli wywołać zdarzenia bezpośrednio.
Nie łam sekwencji cyklu życia
Jeżeli inicjujesz długi łańcuch handlerów, pamiętaj: ktoś może się zapisać i spróbuje zmienić wewnętrzną logikę. Dokumentuj, gdzie i kiedy zdarzenia są wywoływane, i czy zdarzenie może wystąpić wielokrotnie.
Wiele zdarzeń i kompozycja
Kiedy obiekt nasłuchuje wielu źródeł, grupuj subskrypcję i odsubskrypcję w jednym miejscu (np. metody Subscribe/Unsubscribe). W razie potrzeby przechowuj prywatne pola‑delegaty dla wygodnego odsubskrybowania.
3. Jak NIE używać zdarzeń: typowe błędy
Błąd nr 1: ignorowanie standardowego patternu zdarzeń.
Jeśli każda klasa deklaruje swoje własne delegaty dla każdego zdarzenia, szybko robi się chaos. Staraj się używać standardowych delegatów EventHandler lub EventHandler<T>, gdzie T dziedziczy po EventArgs. Taki sposób sprawia, że kod jest bardziej czytelny i naturalny dla .NET‑developerów.
Błąd nr 2: niepoprawne wywoływanie zdarzenia z zewnętrznego kodu.
Nigdy nie wywołuj zdarzenia bezpośrednio poza klasą‑wydawcą. Zdarzenie inicjuje się tylko wewnątrz klasy, zwykle przez chronioną metodę OnEventName. Subskrybenci mają prawo tylko subskrybować (+=) i odsubskrybować (-=).
Źle:
foo.MyEvent(); // błąd! Nie można wywoływać zdarzenia z zewnątrz bezpośrednio
Dobrze:
// Tylko wewnątrz foo:
// protected virtual void OnMyEvent()
// {
// MyEvent?.Invoke(this, EventArgs.Empty);
// }
Błąd nr 3: wywoływanie zdarzenia bez sprawdzenia, czy są subskrybenci (null).
Jeśli spróbujesz wywołać zdarzenie, na które nikt nie jest zapisany, dostaniesz NullReferenceException. W C# 6.0+ użyj ?.Invoke(...) — zdarzenie zostanie wywołane tylko, gdy są subskrybenci.
Błąd nr 4: zapomnienie o odsubskrybowaniu od zdarzenia (wyciek pamięci).
Jeśli obiekt subskrybuje zdarzenie, ale nie odsubskrybuje przed swoim zniszczeniem, wydawca trzyma referencję do subskrybenta. GC nie zwolni pamięci, co jest krytyczne przy długożyjących wydawcach i krótkotrwałych subskrybentach (np. ViewModel, formularze). Najlepszym rozwiązaniem jest jawne odsubskrybowanie lub użycie słabych referencji (WeakReference) tam, gdzie ma to sens.
Błąd nr 5: wywoływać zdarzenie poza chronioną wirtualną metodą OnEvent.
Wywołując zdarzenie bezpośrednio, pozbawiasz klasy pochodne możliwości poprawnego nadpisania zachowania. Prawidłowy pattern to deklaracja protected virtual metody, która wywołuje zdarzenie.
Standardowy przykład:
protected virtual void OnSomething(EventArgs e)
{
Something?.Invoke(this, e);
}
GO TO FULL VERSION