CodeGym /Kursy /C# SELF /Przykłady programowania opartego na zdarzeniach (

Przykłady programowania opartego na zdarzeniach ( event)

C# SELF
Poziom 54 , Lekcja 4
Dostępny

1. Wprowadzenie

Początkujący może zapytać: „Gdzie w praktyce używa się zdarzeń? Czy na serio wszystkie interakcje między klasami opierają się na zdarzeniach, a nie na bezpośrednich wywołaniach metod?” Odpowiedź jest prosta: zdarzenia i delegaty to nie magia rozwiązująca wszystkie problemy architektury. Ale bez nich nowoczesna aplikacja szybko zamienia się w mocno sprzężony system (tightly coupled), gdzie zmiana jednego komponentu wymusza modyfikacje wielu innych. Użycie zdarzeń i delegatów pomaga tworzyć elastyczne, rozszerzalne i łatwe w utrzymaniu systemy.

  • Programowanie UI (WinForms, WPF, Xamarin, MAUI): obsługa kliknięć, najechań, wpisywania tekstu i innych akcji użytkownika.
  • Operacje asynchroniczne: zakończenie pobierania pliku, otrzymanie danych z sieci, uruchomienie timerów.
  • Architektura wtyczek i systemów rozszerzalnych: pozwala podłączać nowe moduły bez twardej integracji z głównym kodem.
  • Systemy sygnalizacyjne/rozesyłające: powiadamianie wielu zainteresowanych komponentów o zaszłych zdarzeniach.
  • Obserwacja zmian stanu („obserwator”): reagowanie na pojawienie się nowej wiadomości, zmianę danych, odświeżenie interfejsu.

A teraz do rzeczy!

2. Szkielet architektoniczny

Wyobraźmy sobie, że piszecie małą konsolową aplikację — "Mini-czat" do wewnętrznych szkoleń w firmie (albo po prostu do ćwiczeń). Mamy byty: Użytkownik, Czat i ewentualnie Bot-pomocnik. Gdy użytkownik wysyła wiadomość, czat musi powiadomić wszystkich podłączonych użytkowników i boty — żeby wyświetlili wiadomość na ekranie albo (w przypadku bota) wygenerowali automatyczną odpowiedź. To klasyczny scenariusz: publisher generuje zdarzenie (event), handlery są przypięte do delegata EventHandler/EventHandler<TEventArgs>, a metoda-handler typu OnMessageReceived reaguje na powiadomienie.


// Klasa User (subskrybent)
public class User
{
    public string Name { get; }

    public User(string name)
    {
        Name = name;
    }

    // Metoda, która będzie handlerem zdarzenia
    public void OnMessageReceived(object? sender, MessageEventArgs e)
    {
        Console.WriteLine($"[{Name}] zobaczył nową wiadomość: {e.MessageText}");
    }
}

// Argumenty zdarzenia
public class MessageEventArgs : EventArgs
{
    public string MessageText { get; }

    public MessageEventArgs(string text) => MessageText = text;
}

// Klasa ChatRoom (wydawca zdarzenia)
public class ChatRoom
{
    public event EventHandler<MessageEventArgs>? MessageReceived;

    public void SendMessage(string text)
    {
        // Generujemy zdarzenie (powiadamiamy wszystkich subskrybentów)
        MessageReceived?.Invoke(this, new MessageEventArgs(text));
    }
}

Przykład użycia:

var chat = new ChatRoom();
var user1 = new User("Antoni");
var user2 = new User("Maria");

chat.MessageReceived += user1.OnMessageReceived;
chat.MessageReceived += user2.OnMessageReceived;

chat.SendMessage("Cześć wszystkim! 😊");

W konsoli zobaczymy dwie wiadomości — obaj użytkownicy dowiedzieli się o nowej wiadomości.

3. Dynamiczna subskrypcja i odsubskrypcja

W praktyce użytkownik może opuścić czat i nie chcieć więcej otrzymywać wiadomości. Nauczmy użytkownika prawidłowo się odsubskrybować (operator -=):

// W klasie User można dodać metodę "Opuść czat"
public void Unsubscribe(ChatRoom chat)
{
    chat.MessageReceived -= OnMessageReceived;
}

Rozwijamy przykład:

var chat = new ChatRoom();
var user1 = new User("Antoni");
var user2 = new User("Maria");

chat.MessageReceived += user1.OnMessageReceived;
chat.MessageReceived += user2.OnMessageReceived;

chat.SendMessage("Pierwsza wiadomość");

user2.Unsubscribe(chat); // Maria opuszcza czat

chat.SendMessage("Maria nie zobaczy już tej wiadomości");

Dynamiczna subskrypcja/odsubskrypcja występuje wszędzie: okna się zamykają, zakładki się zamykają, tymczasowe serwisy odsubskrybowują się od globalnych źródeł zdarzeń. Pamiętajcie — jeśli nie odsubskrybujesz, subskrybent może zostać „zombie”, a aplikacja stanie się źródłem wycieku pamięci!

4. Obsługa wiadomości i generowanie odpowiedzi

Dodajmy teraz bota, który reaguje na każdą wiadomość. Niech bot mówi "Cześć!" automatycznie, jeśli w wiadomości pojawi się słowo "bot". Przy okazji omówimy wielokrotną subskrypcję i demonstrujemy multicast delegates.

public class Bot
{
    public string Name { get; }

    public Bot(string name) => Name = name;

    public void OnMessageReceived(object? sender, MessageEventArgs e)
    {
        // Bot reaguje na słowo kluczowe
        if (e.MessageText.Contains("bot", StringComparison.OrdinalIgnoreCase))
        {
            if (sender is ChatRoom chatRoom)
            {
                Console.WriteLine($"[Bot {Name}]: Witaj! W czym mogę pomóc?");
                // Bot wysyła wiadomość w odpowiedzi
                chatRoom.SendMessage($"Bot {Name} jest gotów pomóc.");
            }
        }
    }
}

Użycie:

var chat = new ChatRoom();
var user = new User("Eugeniusz");
var bot = new Bot("Pomocnik");

chat.MessageReceived += user.OnMessageReceived;
chat.MessageReceived += bot.OnMessageReceived;

// Eugeniusz wysyła wiadomość, która wywołuje bota
chat.SendMessage("Cześć, bot, co słychać?");

Konsola może dać (pokazujemy kolejność wywołań — nie jest ona gwarantowana!):

[Eugeniusz] zobaczył nową wiadomość: Cześć, bot, co słychać?
[Bot Pomocnik]: Witaj! W czym mogę pomóc?
[Eugeniusz] zobaczył nową wiadomość: Bot Pomocnik jest gotów pomóc.
[Bot Pomocnik]: Witaj! W czym mogę pomóc?
[Eugeniusz] zobaczył nową wiadomość: Bot Pomocnik jest gotów pomóc.
[Bot Pomocnik]: Witaj! W czym mogę pomóc?
...

Kto zauważył potencjalny bug? Tak — nieskończona pętla: bot reaguje na własne wiadomości (znowu widzi słowo "bot"). Jednym ze sposobów zapobiegania temu jest proste sprawdzenie:

public void OnMessageReceived(object? sender, MessageEventArgs e)
{
    // Bot nie reaguje na swoje własne wiadomości
    if (e.MessageText.Contains("bot", StringComparison.OrdinalIgnoreCase) &&
        !e.MessageText.Contains(Name))
    {
        if (sender is ChatRoom chatRoom)
        {
            Console.WriteLine($"[Bot {Name}]: Witaj! W czym mogę pomóc?");
            chatRoom.SendMessage($"Bot {Name} jest gotów pomóc.");
        }
    }
}

W praktyce takie sytuacje to dobry moment, żeby pomyśleć o obsłudze zdarzeń cyklicznych, użyciu wyrażeń lambda albo nawet mechanizmie anulowania dalszych handlerów (patrz wcześniejsze lekcje).

5. Operacje asynchroniczne i callbacki

Bardzo często zdarzenia służą do powiadamiania o zakończeniu operacji asynchronicznej. Na przykład pobieranie danych z internetu lub długotrwałe obliczenia: zdarzenie Completed informuje o końcu pracy, a metoda RunLongOperationAsync wykonuje długą operację.

public class LongRunner
{
    // Zdarzenie o zakończeniu pracy
    public event EventHandler<EventArgs>? Completed;

    public async Task RunLongOperationAsync()
    {
        Console.WriteLine("Rozpoczęła się długa operacja...");
        await Task.Delay(2000); // To symulacja długiej pracy
        Console.WriteLine("Operacja zakończona, powiadamiamy subskrybentów.");
        Completed?.Invoke(this, EventArgs.Empty);
    }
}

Kod klienta:

var runner = new LongRunner();

// Subskrypcja zdarzenia zakończenia
runner.Completed += (sender, e) =>
{
    Console.WriteLine("Otrzymano powiadomienie: operacja zakończona!");
};

await runner.RunLongOperationAsync();

To fundament interakcji asynchronicznych — zdarzenia często używane są w frameworkach UI, żeby sygnalizować zakończenie ładowania, koniec animacji, kliknięcia użytkownika itp.

6. Systemy powiadomień

Rozważmy klasyczne zadanie: mamy system powiadomień (np. w sklepie internetowym, gdy na produkt jest promocja). Wszyscy zainteresowani "słuchacze" dowiadują się o tym przez zdarzenie SaleOccurred:

public class SaleNotifier
{
    public event EventHandler<SaleEventArgs>? SaleOccurred;

    public void AnnounceSale(string product, decimal newPrice)
    {
        SaleOccurred?.Invoke(this, new SaleEventArgs(product, newPrice));
    }
}

public class SaleEventArgs : EventArgs
{
    public string Product { get; }
    public decimal NewPrice { get; }
    public SaleEventArgs(string product, decimal price)
    {
        Product = product; NewPrice = price;
    }
}

public class Customer
{
    public string Name { get; }
    public Customer(string name) => Name = name;

    public void OnSale(object? sender, SaleEventArgs e)
    {
        Console.WriteLine($"[{Name}] otrzymał powiadomienie: {e.Product} teraz kosztuje {e.NewPrice.ToString(\"F2\")} jednostek!");
    }
}

Użycie:

var notifier = new SaleNotifier();
var c1 = new Customer("Andrzej");
var c2 = new Customer("Olga");

notifier.SaleOccurred += c1.OnSale;
notifier.SaleOccurred += c2.OnSale;

notifier.AnnounceSale("Czajnik", 999.99m);

Jeśli jeden z klientów nie jest już zainteresowany — odsubskrybowujemy:

notifier.SaleOccurred -= c2.OnSale;
notifier.AnnounceSale("Mikser", 1999.99m);

Taki wzorzec stosuje się do rozsyłania sygnałów (notification, pub/sub), co jest bardzo istotne we współczesnych architekturach.

7. Implementacja mechanizmu anulowania łańcucha zdarzeń

Czasem jeden z handlerów powinien "zatrzymać" dalsze powiadamianie. Zwykle używa się do tego pochodnej EventArgs z flagą anulowania. W przykładzie poniżej ręcznie iterujemy GetInvocationList() i przerywamy wykonanie, gdy Cancel = true.

public class CancelEventArgs : EventArgs
{
    public bool Cancel { get; set; }
}

public class EventSource
{
    public event EventHandler<CancelEventArgs>? SomethingHappened;

    public void DoSomething()
    {
        var args = new CancelEventArgs();
        // Klasyczna pętla do iteracji po subskrybentach (jeśli potrzebna kontrola nad kolejnością)
        var handlers = SomethingHappened?.GetInvocationList();
        if (handlers != null)
        {
            foreach (var handler in handlers)
            {
                ((EventHandler<CancelEventArgs>)handler)(this, args);
                if (args.Cancel)
                {
                    Console.WriteLine("Łańcuch powiadomień przerwany.");
                    break;
                }
            }
        }
    }
}

Użycie:

var source = new EventSource();
source.SomethingHappened += (s, e) =>
{
    Console.WriteLine("Pierwszy obsługujący");
};
source.SomethingHappened += (s, e) =>
{
    Console.WriteLine("Drugi obsługujący anuluje zdarzenie.");
    e.Cancel = true;
};
source.SomethingHappened += (s, e) =>
{
    Console.WriteLine("Ten obsługujący nie zostanie wywołany.");
};

source.DoSomething();

Wynik:

Pierwszy obsługujący
Drugi obsługujący anuluje zdarzenie.
Łańcuch powiadomień przerwany.

Taki mechanizm przydaje się do walidacji, obsługi zdarzeń przed zamknięciem okna, sprawdzania uprawnień i innych zadań, gdzie ważna jest możliwość powiedzenia "Stop! Nie przetwarzamy dalej."

8. Bezpieczeństwo wątków i zarządzanie subskrybentami

W aplikacjach wielowątkowych, gdzie subskrybenci mogą być dodawani i usuwani jednocześnie z generowaniem zdarzenia, warto używać wzorców zapewniających bezpieczeństwo wątkowe (patrz oficjalna dokumentacja). Dla kontroli można zaimplementować zdarzenie ręcznie z akcesorami add i remove i chronić dostęp za pomocą lock:

public class CustomEvent
{
    private EventHandler? _handlers;

    public event EventHandler SomethingHappened
    {
        add
        {
            lock (this) // Bezpieczne w kontekście wątków
            {
                _handlers += value;
            }
        }
        remove
        {
            lock (this)
            {
                _handlers -= value;
            }
        }
    }

    protected void RaiseEvent()
    {
        // Używamy kopii
        EventHandler? handler;
        lock (this)
        {
            handler = _handlers;
        }
        handler?.Invoke(this, EventArgs.Empty);
    }
}

To podejście rzadko się pojawia (zwykle wbudowany mechanizm wystarcza), ale czasami jest potrzebne w systemach o dużym obciążeniu.

9. Zdarzenia w UI — WinForms/WPF/MAUI

WinForms:

private void button1_Click(object sender, EventArgs e)
{
    MessageBox.Show("Przycisk został kliknięty!");
}

Za tą metodą stoi mechanizm subskrypcji:

button1.Click += button1_Click;

WPF/MAUI: zdarzenia typu PropertyChanged (powiadomienia o zmianie właściwości modelu):

public class MyViewModel : INotifyPropertyChanged
{
    private string _value;
    public string Value
    {
        get => _value;
        set
        {
            if (_value != value)
            {
                _value = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value));
            }
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}

Frameworki intensywnie używają zdarzeń do budowania reaktywnych interfejsów (MVVM).

10. Typowe błędy i pułapki przy pracy ze zdarzeniami

Błąd #1: tworzenie wycieków pamięci.
To najczęstszy problem. Jeśli obiekt krótkotrwały (np. okno lub tymczasowy serwis) subskrybuje zdarzenie długotrwałego obiektu (np. globalnego cache'a lub klasy statycznej) i nie odsubskrybuje się przy swoim zniszczeniu, pozostanie na zawsze w pamięci. Zawsze implementuj odsubskrybowanie w metodzie Dispose lub w innym odpowiednim miejscu cyklu życia obiektu.

Błąd #2: tworzenie nieskończonych cykli zdarzeń.
Subskrybent może zareagować na zdarzenie, wywołując akcję, która z kolei znowu generuje to samo zdarzenie. To prowadzi do nieskończonej rekurencji i awarii aplikacji z StackOverflowException. Zawsze sprawdzaj warunki, aby handler nie reagował na zdarzenia, które sam wywołał.

Błąd #3: zależność od kolejności wywołań subskrybentów.
Nigdy nie polegaj na tym, że handlery będą wywoływane w tej samej kolejności, w jakiej zostały subskrybowane. Specyfikacja C# i implementacja CLR tego nie gwarantują. Jeśli kolejność ma znaczenie, wywołuj subskrybentów ręcznie przez GetInvocationList() w pożądanej kolejności.

Błąd #4: niebezpieczna praca ze zdarzeniami w środowisku wielowątkowym.
Jeśli subskrybenci są dodawani lub usuwani z jednego wątku, a zdarzenie jest wywoływane z innego, może wystąpić race condition. Klasyczna ochrona — skopiować delegata do lokalnej zmiennej przed wywołaniem:

var handler = MyEvent;
handler?.Invoke(this, EventArgs.Empty);

Albo użyć lock przy ręcznej implementacji zdarzeń przez add/remove.

2
Zadanie
C# SELF, poziom 54, lekcja 4
Niedostępne
System powiadomień o wyprzedażach
System powiadomień o wyprzedażach
1
Ankieta/quiz
Typowe błędy z delegatami, poziom 54, lekcja 4
Niedostępny
Typowe błędy z delegatami
Najlepsze praktyki programowania zdarzeniowego
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION