1. Wprowadzenie
Przypomnijmy: zdarzenie — to publiczny kontrakt, obietnica twojego kodu, że "zawoła" subskrybentów, kiedy coś ważnego się stanie. W przykładach z poprzednich wykładów mogłeś natknąć się na takie deklaracje:
public event Action<string> MessageSent;
Albo nawet:
public delegate void MyHandler(int value);
public event MyHandler SomethingHappened;
To działa, ale takie podejście rodzi całą kolekcję problemów — zaczynając od rozbieżności w sygnaturze metod po niemożność dowiedzenia się, od kogo przyszło zdarzenie i co dokładnie się stało. Wyobraź sobie, że na klawiaturze przy każdym naciśnięciu klawisz działałby losowo, za każdym razem — inaczej, i to w tym samym programie do tego samego zadania. Powiedzmy, grasz w grę i naciskasz spację. Chwilę temu oznaczała skok, teraz niespodziewanie otwiera ekwipunek. Koszmar, nie standard!
W .NET jest coś w rodzaju Konstytucji dla zdarzeń — to konwencja dotycząca sygnatury handlera i struktury informacji przekazywanej ze zdarzeniem. Oto jak wygląda kanoniczny styl:
void Handler(object sender, EventArgs args);
Znana linijka? Pojawia się wszędzie: od kliknięć przycisku WinForms po systemowe zdarzenia ASP.NET i nawet w zewnętrznych bibliotekach.
2. Czym są EventHandler i EventArgs
Główna idea: każde zdarzenie przekazuje dwie rzeczy:
- Kto wywołał zdarzenie? sender
- Co się stało? EventArgs — dodatkowe dane
W świecie C# to wygląda tak:
public delegate void EventHandler(object sender, EventArgs e);
- object sender — referencja do inicjatora zdarzenia. To może być cokolwiek — po prostu this.
- EventArgs e — obiekt z dodatkowymi informacjami o zdarzeniu. Dla prostych scenariuszy używa się EventArgs.Empty, dla bardziej złożonych — własnych pochodnych EventArgs.
Ciekawostka
W oficjalnych zaleceniach .NET dla publicznych API przyjęte jest, żeby zdarzenie miało sygnaturę (object sender, EventArgs e). Jeśli widzisz zdarzenie bez sender i EventArgs — to najpewniej autorskie uproszczenie, a nie kanoniczny .NET-owy styl.
Ogromna korzyść jednego standardu
Dzięki jednolitemu standardowi zdarzenia łatwiej logować, testować, uniwersalnie się podpinać i rozszerzać system. Otwórz źródła WinForms, WPF, ASP.NET — zobaczysz ten sam wzorzec.
3. Jak używać standardowego szablonu zdarzeń
1. Używamy wbudowanego delegata EventHandler
Zamiast deklarować własny delegat można użyć gotowego:
public event EventHandler SmthHappened;
I teraz handler zawsze wygląda tak samo:
private void OnSmthHappened(object sender, EventArgs e)
{
// Logika reakcji na zdarzenie
}
Subskrypcja nadal jest prosta:
myObj.SmthHappened += OnSmthHappened;
Ważne: jeśli zdarzenie nie przekazuje dodatkowych danych, używaj EventArgs.Empty.
2. Tworzenie własnych argumentów zdarzeń
Jeśli trzeba przekazać informację (wynik obliczeń, nazwę pliku, błąd), stwórz pochodną od EventArgs:
public class CalculationEventArgs : EventArgs
{
public double Result { get; }
public CalculationEventArgs(double result) => Result = result;
}
Następnie używamy uniwersalnego delegata generycznego — EventHandler<TEventArgs>:
public event EventHandler<CalculationEventArgs> CalculationFinished;
I handler teraz przyjmuje dokładnie twój typ argumentów:
private void OnCalculationFinished(object sender, CalculationEventArgs e)
{
Console.WriteLine($"Obliczenie zakończone. Wynik: {e.Result}");
}
4. Przykład aplikacji
Rozwijamy nasz edukacyjny projekt — niech będzie kalkulator, który dodaje lub odejmuje liczby i informuje o zakończeniu operacji przez zdarzenie.
Minimalistyczny przykład
public class Calculator
{
public event EventHandler<CalculationEventArgs> CalculationPerformed;
public void Add(int a, int b)
{
int result = a + b;
// "Wystrzeliwujemy" zdarzenie
CalculationPerformed?.Invoke(this, new CalculationEventArgs(result));
}
}
public class CalculationEventArgs : EventArgs
{
public int Result { get; }
public CalculationEventArgs(int result) => Result = result;
}
Subskrybujemy i używamy:
var calc = new Calculator();
calc.CalculationPerformed += (sender, e) =>
{
Console.WriteLine($"Wynik operacji: {e.Result}");
};
calc.Add(10, 20);
// Wypisze: Wynik operacji: 30
To cały "standard": handler zawsze przyjmuje obiekt-nadawcę i obiekt argumentów — uniwersalnie i czytelnie.
5. Przydatne niuanse
Enkapsulacja wywołania zdarzenia: dobry styl
W .NET przyjęte jest wydzielanie logiki wywołania zdarzenia do osobnej chronionej metody z prefiksem On:
protected virtual void OnCalculationPerformed(CalculationEventArgs e)
{
CalculationPerformed?.Invoke(this, e);
}
A w logice ("Add", "Subtract" itd.) po prostu wywołujemy tę metodę:
public void Add(int a, int b) => OnCalculationPerformed(new CalculationEventArgs(a + b));
Taki styl pozwala potomkom nadpisywać zachowanie zdarzenia i zmniejsza szansę zapomnienia o jego wywołaniu.
Schemat: Jak zbudowane jest zdarzenie według standardu .NET
graph LR
A[Obiekt-wydawca] -- "event EventHandler/ EventHandler<TEventArgs>" --> B[Lista subskrybentów]
B -- "Metoda-handler (object sender, EventArgs e)" --> C[Reakcja na zdarzenie]
A -- "this (nadawca)" --> C
A -- "EventArgs (dane)" --> C
Porównanie wariantów deklarowania zdarzeń
| Podejście | Przekazanie inicjatora (sender) | Przekazanie argumentów | Uniwersalność | Użycie w .NET |
|---|---|---|---|---|
|
Nie | Tak | Niska | Nie |
|
Tak | Tak | Średnia | Rzadko |
|
Tak | Nie (EventArgs) | Wysoka | Tak, standard |
|
Tak | Tak (MyArgs) | Bardzo wysoka | Tak, standard |
Praktyczna korzyść na rozmowach rekrutacyjnych i w produkcyjnym kodzie
Używanie standardowego szablonu zdarzeń — must-have dla .NET-developera. Na rozmowie prawie na pewno zapytają cię o EventHandler i parę sender/EventArgs. Zdarzenie typu Action<T> często jest postrzegane jako "uproszczenie".
W realnych projektach takie podejście ułatwia współpracę, testowanie, rozszerzalność i utrzymanie kodu. Zewnętrzne biblioteki (logowanie, profilery) łatwiej się integrują, kiedy używany jest standardowy format.
Schemat blokowy wywołania zdarzenia
flowchart TD
subgraph A[Klasa-wydawca]
C1((Obiekt))
C2[Metoda wywołująca zdarzenie]
C3["event EventHandler<MyArgs>"]
end
subgraph B[Klasa-subskrybent]
D1((Subskrypcja))
D2["Handler zdarzenia (object sender, MyArgs args)"]
end
C1 -- Wywołuje --> C2
C2 -- "Invoke(this, args)" --> C3
C3 -- "Powiadamia" --> D1
D1 -- "Wywołuje" --> D2
6. Porady, niuanse i typowe błędy
1. Sygnatura handlera. Chcesz zrobić zdarzenie typu Action<int>? To kuszące, ale tracisz sender i kompatybilność z ekosystemem.
2. Przekazywanie argumentów. Nie myl EventArgs z zwykłymi parametrami. Wszystkie dane dla handlera przekazuj w obiekcie argumentów.
3. Używanie null dla argumentów. Zamiast null używaj EventArgs.Empty, jeśli nie ma dodatkowych danych.
4. Słaba typizacja. Nie rób jednego "wszystkożernego" zdarzenia EventHandler i nie wrzucaj tam wszystkiego. Stwórz oddzielną klasę-pochodną dla każdego zdarzenia — czytelność i niezawodność rosną.
5. Błędy przy wywoływaniu zdarzenia. Zawsze sprawdzaj obecność subskrybentów: SomeEvent?.Invoke(this, e). Bez subskrybentów referencja zdarzenia jest równa null.
6. Naruszenie enkapsulacji. Nie wywołuj zdarzenia z zewnątrz klasy-wydawcy. Zdarzenie — tylko do subskrypcji/odsubskrypcji; wywołuj je wewnątrz przez metodę On....
GO TO FULL VERSION