CodeGym /Kurse /C# SELF /Beispiele für eventbasiertes Programmieren (

Beispiele für eventbasiertes Programmieren ( event)

C# SELF
Level 54, Lektion 4
Verfügbar

1. Einführung

Ein typischer Anfänger könnte fragen: „Wo werden Ereignisse praktisch eingesetzt? Werden alle Interaktionen zwischen Klassen über Ereignisse und nicht über direkte Methodenaufrufe gelöst?“ Die Antwort ist einfach: Ereignisse und Delegates sind kein Zauber, der alle Architekturprobleme löst. Ohne sie verwandelt sich eine moderne Anwendung schnell in stark gekoppelte (tightly coupled) Komponenten, bei denen eine Änderung an einer Stelle viele andere Änderungen erfordert. Der Einsatz von Ereignissen und Delegates hilft, flexible, erweiterbare und wartbare Systeme zu erstellen.

  • UI-Programmierung (WinForms, WPF, Xamarin, MAUI): Verarbeitung von Klicks, Hover, Texteingabe und anderen Benutzeraktionen.
  • Asynchrone Operationen: Abschluss eines Datei-Downloads, Empfang von Netzwerkdaten, Auslösen von Timern.
  • Plugin- und erweiterbare Systemarchitekturen: erlauben das Anschließen neuer Module ohne starre Integration in den Hauptcode.
  • Signalisierende/Publish-Subscribe-Systeme: Benachrichtigung vieler interessierter Komponenten über aufgetretene Ereignisse.
  • Beobachtung von Zustandsänderungen („Observer“): Reagieren auf neue Nachrichten, Datenänderungen, UI-Updates.

Und jetzt — zur Sache!

2. Architektonisches Skelett

Stellen wir uns vor, ihr entwickelt eine kleine Konsolenanwendung — einen "Mini-Chat" für internes Lernen im Unternehmen (oder einfach zum Üben). Wir haben Entitäten: User, Chat und eventuell einen Bot-Assistant. Wenn ein User eine Nachricht sendet, sollte der Chat alle verbundenen User und Bots benachrichtigen — damit sie die Nachricht anzeigen oder im Fall eines Bots eine automatische Antwort generieren. Das ist ein klassisches Szenario: der Publisher feuert ein Ereignis (event), Handler sind auf den Delegate EventHandler/EventHandler<TEventArgs> abonniert, und die Handler-Methode wie OnMessageReceived reagiert auf die Benachrichtigung.

// Klasse User (Subscriber)
public class User
{
    public string Name { get; }

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

    // Methode, die als Event-Handler dient
    public void OnMessageReceived(object? sender, MessageEventArgs e)
    {
        Console.WriteLine($"[{Name}] hat eine neue Nachricht gesehen: {e.MessageText}");
    }
}

// Event-Argumente
public class MessageEventArgs : EventArgs
{
    public string MessageText { get; }

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

// Chat-Klasse (Publisher)
public class ChatRoom
{
    public event EventHandler<MessageEventArgs>? MessageReceived;

    public void SendMessage(string text)
    {
        // Wir feuern das Ereignis (benachrichtigen alle Abonnenten)
        MessageReceived?.Invoke(this, new MessageEventArgs(text));
    }
}

Beispiel zur Verwendung:

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

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

chat.SendMessage("Hallo zusammen! 😊");

In der Konsole sehen wir zwei Meldungen — beide User haben von der neuen Nachricht erfahren.

3. Dynamisches Subscribe und Unsubscribe

In der Realität kann ein User den Chat verlassen und keine Nachrichten mehr erhalten wollen. Lehren wir einen User, sich korrekt abzumelden (Operator -=):

// In der Klasse User kann man eine Methode "Unsubscribe" hinzufügen
public void Unsubscribe(ChatRoom chat)
{
    chat.MessageReceived -= OnMessageReceived;
}

Erweitern wir das Beispiel:

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

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

chat.SendMessage("Erste Nachricht");

user2.Unsubscribe(chat); // Maria verlässt den Chat

chat.SendMessage("Maria wird diese Nachricht nicht mehr sehen");

Dynamisches Subscribe/Unsubscribe kommt überall vor: Fenster schließen sich, Tabs werden geschlossen, temporäre Services unsubscriben von globalen Event-Quellen. Vergesst nicht — ein nicht abgemeldeter Subscriber wird zum Zombie und die Anwendung zum Memory Leaker!

4. Nachrichtverarbeitung und Antwortgenerierung

Jetzt fügen wir einen Bot hinzu, der auf jede Nachricht reagiert. Der Bot sagt automatisch "Hallo!", wenn die Nachricht das Wort "bot" enthält. Gleichzeitig betrachten wir Mehrfach-Subscriptions und Multi-Cast-Delegates.

public class Bot
{
    public string Name { get; }

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

    public void OnMessageReceived(object? sender, MessageEventArgs e)
    {
        // Bot reagiert auf das Schlüsselwort
        if (e.MessageText.Contains("bot", StringComparison.OrdinalIgnoreCase))
        {
            if (sender is ChatRoom chatRoom)
            {
                Console.WriteLine($"[Bot {Name}]: Guten Tag! Womit kann ich helfen?");
                // Bot sendet eine Antwort
                chatRoom.SendMessage($"Bot {Name} ist bereit zu helfen.");
            }
        }
    }
}

Verwendung:

var chat = new ChatRoom();
var user = new User("Eugene");
var bot = new Bot("Assistant");

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

// Eugene schreibt eine Nachricht, die den Bot triggert
chat.SendMessage("Hallo, bot, wie geht's?");

Die Konsole könnte ausgeben (Aufrufreihenfolge ist nicht garantiert!):

[Eugene] hat eine neue Nachricht gesehen: Hallo, bot, wie geht's?
[Bot Assistant]: Guten Tag! Womit kann ich helfen?
[Eugene] hat eine neue Nachricht gesehen: Bot Assistant ist bereit zu helfen.
[Bot Assistant]: Guten Tag! Womit kann ich helfen?
[Eugene] hat eine neue Nachricht gesehen: Bot Assistant ist bereit zu helfen.
[Bot Assistant]: Guten Tag! Womit kann ich helfen?
...

Wer hat den potentiellen Bug bemerkt? Richtig, eine Endlosschleife: der Bot reagiert auf seine eigenen Nachrichten (er sieht wieder das Wort "bot"). Eine Möglichkeit, das zu verhindern, ist eine einfache Prüfung:

public void OnMessageReceived(object? sender, MessageEventArgs e)
{
    // Bot reagiert nicht auf eigene Nachrichten
    if (e.MessageText.Contains("bot", StringComparison.OrdinalIgnoreCase) &&
        !e.MessageText.Contains(Name))
    {
        if (sender is ChatRoom chatRoom)
        {
            Console.WriteLine($"[Bot {Name}]: Guten Tag! Womit kann ich helfen?");
            chatRoom.SendMessage($"Bot {Name} ist bereit zu helfen.");
        }
    }
}

Solche Situationen sind in der Praxis ein guter Anlass, über zyklische Event-Verarbeitung, Lambda-Ausdrücke oder sogar Mechanismen zum Abbrechen weiterer Handler nachzudenken (siehe frühere Vorlesungen).

5. Asynchrone Operationen und Callbacks

Sehr oft werden Events verwendet, um über den Abschluss asynchroner Operationen zu informieren. Zum Beispiel Datenladen aus dem Internet oder eine lange Berechnung: das Event Completed meldet das Ende, und die Methode RunLongOperationAsync führt eine lange Operation aus.

public class LongRunner
{
    // Ereignis über Abschluss
    public event EventHandler<EventArgs>? Completed;

    public async Task RunLongOperationAsync()
    {
        Console.WriteLine("Lange Operation hat begonnen...");
        await Task.Delay(2000); // Das simuliert lange Arbeit
        Console.WriteLine("Operation abgeschlossen, benachrichtige Abonnenten.");
        Completed?.Invoke(this, EventArgs.Empty);
    }
}

Client-Code:

var runner = new LongRunner();

// Subscribe auf das Abschluss-Ereignis
runner.Completed += (sender, e) =>
{
    Console.WriteLine("Benachrichtigung erhalten: Operation ist abgeschlossen!");
};

await runner.RunLongOperationAsync();

Das ist die Grundlage asynchroner Interaktion — Events werden oft in UI-Frameworks verwendet, um das Ende von Ladevorgängen, Animationen, Benutzerklicks usw. zu signalisieren.

6. Signal- bzw. Notification-Systeme

Betrachten wir eine weitere klassische Aufgabe: ein Benachrichtigungssystem (z.B. im Onlineshop, wenn ein Produkt reduziert ist). Alle interessierten "Listener" erfahren davon via Event 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}] hat eine Benachrichtigung erhalten: {e.Product} kostet jetzt {e.NewPrice.ToString(\"F2\")} Einheiten!");
    }
}

Verwendung:

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

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

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

Wenn ein Kunde nicht mehr interessiert ist — unsubscriben:

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

Dieses Pattern wird für Signalaussendungen (notification, pub/sub) verwendet und ist in modernen Architekturen sehr wichtig.

7. Implementierung eines Abbruchmechanismus für die Event-Kette

Manchmal muss einer der Handler die weitere Benachrichtigung "stoppen". Dafür verwendet man üblicherweise eine von EventArgs abgeleitete Klasse mit einem Abbruch-Flag. Im folgenden Beispiel iterieren wir manuell über GetInvocationList() und brechen ab, wenn 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();
        // Klassische Schleife zum Durchlaufen der Abonnenten (wenn Reihenfolge/Kontrolle nötig)
        var handlers = SomethingHappened?.GetInvocationList();
        if (handlers != null)
        {
            foreach (var handler in handlers)
            {
                ((EventHandler<CancelEventArgs>)handler)(this, args);
                if (args.Cancel)
                {
                    Console.WriteLine("Die Handler-Kette wurde abgebrochen.");
                    break;
                }
            }
        }
    }
}

Verwendung:

var source = new EventSource();
source.SomethingHappened += (s, e) =>
{
    Console.WriteLine("Erster Handler");
};
source.SomethingHappened += (s, e) =>
{
    Console.WriteLine("Zweiter Handler bricht das Ereignis ab.");
    e.Cancel = true;
};
source.SomethingHappened += (s, e) =>
{
    Console.WriteLine("Dieser Handler wird nicht aufgerufen werden.");
};

source.DoSomething();

Ergebnis:

Erster Handler
Zweiter Handler bricht das Ereignis ab.
Die Handler-Kette wurde abgebrochen.

So ein Mechanismus wird für Validierung, Behandlung von Ereignissen vor dem Schließen eines Fensters, Berechtigungsprüfungen und andere Aufgaben benötigt, bei denen man sagen muss: "Stopp! Weiter nicht verarbeiten."

8. Thread-Safety und Verwaltung von Abonnenten

In Multithreaded-Anwendungen, wo Abonnenten hinzugefügt und entfernt werden können, während ein Ereignis ausgelöst wird, ist es wichtig, thread-sichere Muster zu verwenden (siehe offizielle Dokumentation). Zur Kontrolle kann man das Event manuell mit den Accessors add und remove implementieren und den Zugriff mit lock schützen:

public class CustomEvent
{
    private EventHandler? _handlers;

    public event EventHandler SomethingHappened
    {
        add
        {
            lock (this) // Thread-sicher
            {
                _handlers += value;
            }
        }
        remove
        {
            lock (this)
            {
                _handlers -= value;
            }
        }
    }

    protected void RaiseEvent()
    {
        // Wir verwenden eine Kopie
        EventHandler? handler;
        lock (this)
        {
            handler = _handlers;
        }
        handler?.Invoke(this, EventArgs.Empty);
    }
}

Dieser Ansatz ist selten nötig (meist reicht der Standardmechanismus), aber manchmal erforderlich für stark ausgelastete Systeme.

9. Events im UI — WinForms/WPF/MAUI

WinForms:

private void button1_Click(object sender, EventArgs e)
{
    MessageBox.Show("Button wurde gedrückt!");
}

Hinter dieser Methode steckt der Subscribe-Mechanismus:

button1.Click += button1_Click;

WPF/MAUI: Events wie PropertyChanged (Benachrichtigung über Änderungen an Model-Eigenschaften):

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;
}

Frameworks nutzen Events aktiv, um reaktive Interfaces (MVVM) aufzubauen.

10. Typische Fehler und Stolperfallen bei Events

Fehler Nr.1: Memory Leaks.
Das ist das häufigste Problem. Wenn ein kurzlebiges Objekt (z.B. ein Fenster oder ein temporärer Service) sich bei einem langlebigen Objekt (z.B. globaler Cache oder statische Klasse) subscribed und sich beim Zerstören nicht unsubscribet, bleibt es für immer im Speicher. Implementiert immer Unsubscribe in Dispose oder an einer passenden Stelle im Lebenszyklus des Objekts.

Fehler Nr.2: Endlose Event-Zyklen.
Ein Subscriber kann auf ein Event reagieren, indem er eine Aktion auslöst, die dasselbe Event erneut feuert. Das führt zu unendlicher Rekursion und Absturz der App mit StackOverflowException. Prüft immer Bedingungen, damit der Handler nicht auf Events reagiert, die er selbst ausgelöst hat.

Fehler Nr.3: Abhängigkeit von der Aufrufreihenfolge der Subscriber.
Verlasst euch niemals darauf, dass Event-Handler in der Reihenfolge aufgerufen werden, in der sie subscribet wurden. Die C#-Spezifikation und die CLR-Implementierung garantieren das nicht. Wenn die Reihenfolge wichtig ist, ruft die Subscriber manuell über GetInvocationList() in der gewünschten Reihenfolge auf.

Fehler Nr.4: Unsichere Arbeit mit Events in Multithreading-Umgebungen.
Wenn Subscriber in einem Thread hinzugefügt oder entfernt werden, während das Event in einem anderen Thread gefeuert wird, kann eine Race Condition entstehen. Klassischer Schutz — kopiert den Delegate in eine lokale Variable vor dem Aufruf:

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

Oder verwendet lock, wenn ihr Events manuell via add/remove implementiert.

2
Aufgabe
C# SELF, Level 54, Lektion 4
Gesperrt
Benachrichtigungssystem für Ausverkäufe
Benachrichtigungssystem für Ausverkäufe
1
Umfrage/Quiz
Typische Fehler mit Delegates, Level 54, Lektion 4
Nicht verfügbar
Typische Fehler mit Delegates
Best Practices für Event-basiertes Programmieren
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION